[Study Notes] Introduction to Principles of Webpack5 in Shang Silicon Valley | Advanced Articles

Advanced WebPack5

⛄Continue to study the advanced chapter of WebPack5.

⛄This article mainly talks about the idea of ​​WebPack5 packaging optimization, all configuration items, no technical content, mainly the learning of ideas.

⭐Note : This article is a record of study notes for the Webpack5 tutorial of Shang Silicon Valley Web front-end , adding some of my own practice changes and thinking.

⭐Recommend everyone to watch the original video : Getting Started with Webpack5 in Silicon Valley to Principles (One-stop Interview and Development)_哔哩哔哩_bilibili

my summary

  • The following are some configuration items, here is a general description of the optimization ideas

Developer experience optimization

  • SourceMap: Error reporting after packaging can map source code error reporting location

Package acceleration

  • HotModuleReplacement: hot module replacement
  • OneOf: regular matching optimization
  • Include/Exclude: Match the files that need to be packaged, and filter the unnecessary ones
  • Cache: Cache optimization, caching the results of Eslint inspection and Babel compilation
  • Thread: multi-process packaging

Compress code size

  • TreeShaking: Only package the referenced content of the required library, instead of packaging the entire library
  • Babel: there is a plugin to optimize Babel volume
  • ImageMinimizer: Packed image compression

compatibility

  • Core-js: babel patch, which can make ES6 syntax and other content such as asyc function and promise object backward compatible

Browser rendering optimization

  • CodeSplit: Split JS files, load them on demand, and import whichever library you need. Instead of importing them all directly.
  • Preload/Prefetch: resources can be loaded during browser idle time
  • NetworkCache: cache requested resources
  • PWA: Projects can still be accessed when offline

⭐It is necessary to configure a lot of content. I think this chapter does not need to look at how to configure, but only needs to go through the concepts and optimization ideas.

⭐When you need it, just go to the official website to find the latest configuration according to this idea.

introduce

This chapter mainly introduces the advanced configuration of Webpack.

The so-called advanced configuration is actually Webpack optimization, so that our code has better performance when compiling/running~

We will optimize from the following perspectives:

  1. Improve development experience
  2. Improve package build speed
  3. Reduce code size
  4. Optimize code execution performance

Improve development experience

SourceMap

Why

The code we run during development is compiled by webpack, for example, it looks like this:

/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
/******/ (() => {
    
     // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({
    
    

/***/ "./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/less/index.less":
/*!**********************************************************************************************************!*\
  !*** ./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/less/index.less ***!
  \**********************************************************************************************************/
/***/ ((module, __webpack_exports__, __webpack_require__) => {
    
    

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports\n\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".box2 {\\n  width: 100px;\\n  height: 100px;\\n  background-color: deeppink;\\n}\\n\", \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://webpack5/./src/less/index.less?./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js");

/***/ }),
// 其他省略

All css and js merged into one file, and additional code. At this time, if the code runs incorrectly, we cannot understand the error location of the code. Once a lot of code files are developed in the future, it is difficult to find where the error occurs.

So we need more accurate error prompts to help us develop code better.

what is

SourceMap (source code mapping) is a scheme used to generate a one-to-one mapping file between source code and built code.

It will generate a xxx.map file, which contains the mapping relationship between each row and each column of the source code and the built code. When there is an error in the code after construction, the xxx.map file will be used to find the error location of the source code after the mapping from the error location of the code after construction, so that the browser will prompt the error location of the source code file, helping us find the source of the error faster.

how to use

By looking at the Webpack DevTool document open in new window , we can see that there are many situations for the value of SourceMap.

But in actual development, we only need to pay attention to two situations:

  • Development mode:cheap-module-source-map
    • Advantages: fast package compilation, only contains line mapping
    • Cons: no column mapping
module.exports = {
    
    
  // 其他省略
  mode: "development",
  devtool: "cheap-module-source-map",
};
  • Production mode:source-map
    • Pros: Contains row/column mapping
    • Disadvantages: package compilation is slower
module.exports = {
    
    
  // 其他省略
  mode: "production",
  devtool: "source-map",
};

In short, it is recommended to use it in development mode cheap-module-source-mapand use it in production mode source-map.

Improve package build speed

HotModuleReplacement

Why

  • We modified one of the module codes during development, and Webpack will repackage and compile all modules by default, which is very slow.
  • So we need to modify the code of a certain module, only the code of this module needs to be repackaged and compiled, and the other modules remain unchanged, so that the packaging speed can be very fast.

what is

HotModuleReplacement (HMR/Hot Module Replacement): Replace, add, or delete modules while the program is running without reloading the entire page.

how to use

basic configuration
  • This configuration is enabled by default in WebPack5
module.exports = {
    
    
  // 其他省略
  devServer: {
    
    
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
    hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)
  },
};
JS configuration
  • By default, JS cannot be replaced by hot modules. After modifying the above configuration, we found that modifying JS will still refresh the entire page.
  • You need to use module.hot.accept to enable js to enable hot loading, but you need to judge whether it supports module.hot
// main.js


// 判断是否支持HMR功能
if (module.hot) {
    
    
    module.hot.accept("./js/count.js", function (count) {
    
    
        const result1 = count(2, 1);
        console.log(result1);
    });

    module.hot.accept("./js/sum.js", function (sum) {
    
    
        const result2 = sum(1, 2, 3, 4);
        console.log(result2);
    });
}

It will be troublesome to write the above, so we will use other loaders to solve the actual development.

For example: vue-loader , react-hot-loader .

Add the above loader to automatically configure js hot loading.

OneOf

Why

When packaging, each file will be processed by all loaders. Although it is not actually processed because of the testregularity , it must be processed once. slower.

what is

As the name implies, it can only match the last loader, and the rest will not match.

how to use

  • Configure the oneof array in the rules, and store the matching values ​​in the array.
  • Originally, all the rules will be traversed to match, but in fact, the regular rules we write are actually one-to-one correspondence. After adding them to OneOf, they will not match when they match one other.
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    
    
  entry: "./src/main.js",
  output: {
    
    
    path: undefined, // 开发模式没有输出,不需要指定输出目录
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    // clean: true, // 开发模式没有输出,不需要清空输出结果
  },
  module: {
    
    
    rules: [
      {
    
    
        oneOf: [
          {
    
    
            // 用来匹配 .css 结尾的文件
            test: /\.css$/,
            // use 数组里面 Loader 执行顺序是从右到左
            use: ["style-loader", "css-loader"],
          },
          {
    
    
            test: /\.less$/,
            use: ["style-loader", "css-loader", "less-loader"],
          },
          {
    
    
            test: /\.s[ac]ss$/,
            use: ["style-loader", "css-loader", "sass-loader"],
          },
          {
    
    
            test: /\.styl$/,
            use: ["style-loader", "css-loader", "stylus-loader"],
          },
          {
    
    
            test: /\.(png|jpe?g|gif|webp)$/,
            type: "asset",
            parser: {
    
    
              dataUrlCondition: {
    
    
                maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
              },
            },
            generator: {
    
    
              // 将图片文件输出到 static/imgs 目录中
              // 将图片文件命名 [hash:8][ext][query]
              // [hash:8]: hash值取8位
              // [ext]: 使用之前的文件扩展名
              // [query]: 添加之前的query参数
              filename: "static/imgs/[hash:8][ext][query]",
            },
          },
          {
    
    
            test: /\.(ttf|woff2?)$/,
            type: "asset/resource",
            generator: {
    
    
              filename: "static/media/[hash:8][ext][query]",
            },
          },
          {
    
    
            test: /\.js$/,
            exclude: /node_modules/, // 排除node_modules代码不编译
            loader: "babel-loader",
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
    
    
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
    }),
    new HtmlWebpackPlugin({
    
    
      // 以 public/index.html 为模板创建文件
      // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
      template: path.resolve(__dirname, "../public/index.html"),
    }),
  ],
  // 开发服务器
  devServer: {
    
    
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
    hot: true, // 开启HMR功能
  },
  mode: "development",
  devtool: "cheap-module-source-map",
};

Include/Exclude

Why

We need to use third-party libraries or plug-ins during development, and all files are downloaded to node_modules. And these files do not need to be compiled and can be used directly.

So when we process js files, we need to exclude the files under node_modules.

what is

  • include

Include, only process xxx files

  • exclude

Exclude, all files except xxx files are processed

how to use

const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    
    
    entry: "./src/main.js",
    output: {
    
    
        path: undefined, // 开发模式没有输出,不需要指定输出目录
        filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
        // clean: true, // 开发模式没有输出,不需要清空输出结果
    },
    module: {
    
    
        rules: [
            {
    
    
                oneOf: [
                    {
    
    
                        // 用来匹配 .css 结尾的文件
                        test: /\.css$/,
                        // use 数组里面 Loader 执行顺序是从右到左
                        use: ["style-loader", "css-loader"],
                    },
                    {
    
    
                        test: /\.less$/,
                        use: ["style-loader", "css-loader", "less-loader"],
                    },
                    {
    
    
                        test: /\.s[ac]ss$/,
                        use: ["style-loader", "css-loader", "sass-loader"],
                    },
                    {
    
    
                        test: /\.styl$/,
                        use: ["style-loader", "css-loader", "stylus-loader"],
                    },
                    {
    
    
                        test: /\.(png|jpe?g|gif|webp)$/,
                        type: "asset",
                        parser: {
    
    
                            dataUrlCondition: {
    
    
                                maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
                            },
                        },
                        generator: {
    
    
                            // 将图片文件输出到 static/imgs 目录中
                            // 将图片文件命名 [hash:8][ext][query]
                            // [hash:8]: hash值取8位
                            // [ext]: 使用之前的文件扩展名
                            // [query]: 添加之前的query参数
                            filename: "static/imgs/[hash:8][ext][query]",
                        },
                    },
                    {
    
    
                        test: /\.(ttf|woff2?)$/,
                        type: "asset/resource",
                        generator: {
    
    
                            filename: "static/media/[hash:8][ext][query]",
                        },
                    },
                    {
    
    
                        test: /\.js$/,
                        // exclude: /node_modules/, // 排除node_modules代码不编译
                        include: path.resolve(__dirname, "../src"), // 也可以用包含
                        loader: "babel-loader",
                    },
                ],
            },
        ],
    },
    // ....
};

Production mode is also configured like this.

Cache

Why

The js file must be checked by Eslint and compiled by Babel every time it is packaged, which is relatively slow.

We can cache the results of previous Eslint checks and Babel compilation, so that the speed of the second packaging will be faster.

what is

Caching of Eslint checks and Babel compilation results

how to use

const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    
    
    entry: "./src/main.js",
    output: {
    
    
        path: undefined, // 开发模式没有输出,不需要指定输出目录
        filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
        // clean: true, // 开发模式没有输出,不需要清空输出结果
    },
    module: {
    
    
        rules: [
            {
    
    
                oneOf: [
                    // ...
                    {
    
    
                        test: /\.js$/,
                        // exclude: /node_modules/, // 排除node_modules代码不编译
                        include: path.resolve(__dirname, "../src"), // 也可以用包含
                        loader: "babel-loader",
                        options: {
    
    
                            cacheDirectory: true, // 开启babel编译缓存
                            cacheCompression: false, // 缓存文件不要压缩
                        },
                    },
                ],
            },
        ],
    },
    plugins: [
        new ESLintWebpackPlugin({
    
    
            // 指定检查文件的根目录
            context: path.resolve(__dirname, "../src"),
            exclude: "node_modules", // 默认值


            cache: true, // 开启缓存
            // 缓存目录
            cacheLocation: path.resolve(
                __dirname,
                "../node_modules/.cache/.eslintcache"
            ),
        }),

        // ... 
    ],
    // ...
};

Thead

Why

When the project becomes larger and larger, the packaging speed becomes slower and slower, and it even takes an afternoon to package the code. This speed is relatively slow.

We want to continue to improve the packaging speed, in fact, we want to improve the packaging speed of js, because there are relatively few other files.

The processing of js files mainly includes three tools: eslint, babel, and Terser, so we need to improve their running speed.

We can open multiple processes to process js files at the same time, so that the speed is faster than the previous single-process packaging.

what is

Multi-process packaging: open multiple processes of the computer to do one thing at the same time, which is faster.

Note: Please only use it in particularly time-consuming operations, because each process startup has an overhead of about 600ms.

how to use

The number of processes we start is the number of cores of our CPU.

  • How to get the number of CPU cores, because every computer is different.
// nodejs核心模块,直接使用
const os = require("os");
// cpu核数
const threads = os.cpus().length;
  • download package
npm i thread-loader -D
  • use
const os = require("os"); // 添加此代码
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");

// cpu核数
const threads = os.cpus().length; // 添加此代码

// 获取处理样式的Loaders
const getStyleLoaders = (preProcessor) => {
    
    
    return [
        MiniCssExtractPlugin.loader,
        "css-loader",
        {
    
    
            loader: "postcss-loader",
            options: {
    
    
                postcssOptions: {
    
    
                    plugins: [
                        "postcss-preset-env", // 能解决大多数样式兼容性问题
                    ],
                },
            },
        },
        preProcessor,
    ].filter(Boolean);
};

module.exports = {
    
    
    entry: "./src/main.js",
    output: {
    
    
        path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
        filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
        clean: true,
    },
    module: {
    
    
        rules: [
            {
    
    
                oneOf: [
                    {
    
    
                        // 用来匹配 .css 结尾的文件
                        test: /\.css$/,
                        // use 数组里面 Loader 执行顺序是从右到左
                        use: getStyleLoaders(),
                    },
                    {
    
    
                        test: /\.less$/,
                        use: getStyleLoaders("less-loader"),
                    },
                    {
    
    
                        test: /\.s[ac]ss$/,
                        use: getStyleLoaders("sass-loader"),
                    },
                    {
    
    
                        test: /\.styl$/,
                        use: getStyleLoaders("stylus-loader"),
                    },
                    {
    
    
                        test: /\.(png|jpe?g|gif|webp)$/,
                        type: "asset",
                        parser: {
    
    
                            dataUrlCondition: {
    
    
                                maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
                            },
                        },
                        generator: {
    
    
                            // 将图片文件输出到 static/imgs 目录中
                            // 将图片文件命名 [hash:8][ext][query]
                            // [hash:8]: hash值取8位
                            // [ext]: 使用之前的文件扩展名
                            // [query]: 添加之前的query参数
                            filename: "static/imgs/[hash:8][ext][query]",
                        },
                    },
                    {
    
    
                        test: /\.(ttf|woff2?)$/,
                        type: "asset/resource",
                        generator: {
    
    
                            filename: "static/media/[hash:8][ext][query]",
                        },
                    },
                    {
    
    
                        test: /\.js$/,
                        // exclude: /node_modules/, // 排除node_modules代码不编译
                        include: path.resolve(__dirname, "../src"), // 也可以用包含
                        // 加入以下代码
                        use: [
                            {
    
    
                                loader: "thread-loader", // 开启多进程
                                options: {
    
    
                                    workers: threads, // 数量
                                },
                            },
                            {
    
    
                                loader: "babel-loader",
                                options: {
    
    
                                    cacheDirectory: true, // 开启babel编译缓存
                                },
                            },
                        ],
                    },
                ],
            },
        ],
    },
    plugins: [
        new ESLintWebpackPlugin({
    
    
            // 指定检查文件的根目录
            context: path.resolve(__dirname, "../src"),
            exclude: "node_modules", // 默认值
            cache: true, // 开启缓存
            // 缓存目录
            cacheLocation: path.resolve(
                __dirname,
                "../node_modules/.cache/.eslintcache"
            ),
            // 添加此代码 ESlint
            threads, // 开启多进程
        }),
        new HtmlWebpackPlugin({
    
    
            // 以 public/index.html 为模板创建文件
            // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
            template: path.resolve(__dirname, "../public/index.html"),
        }),
        // 提取css成单独文件
        new MiniCssExtractPlugin({
    
    
            // 定义输出文件名和目录
            filename: "static/css/main.css",
        }),
        // css压缩
        // new CssMinimizerPlugin(),
    ],
    // CSS压缩也可以开启多进程
    optimization: {
    
    
        minimize: true,
        minimizer: [
            // css压缩也可以写到optimization.minimizer里面,效果一样的
            new CssMinimizerPlugin(),
            // 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
            new TerserPlugin({
    
    
                parallel: threads // 开启多进程
            })
        ],
    },
    // ...
};

Reduce code size

TreeShaking

Why

  • During development, we defined some tool function libraries, or referenced third-party tool function libraries or component libraries.
  • If there is no special treatment, we will introduce the entire library when packaging, but in fact we may only use a very small part of the functions .
  • In this way, the entire library is packaged in, and the volume is too large .

what is

Tree Shakingis a term commonly used to describe the removal of unused code in JavaScript.

Note: it depends ES Module.

how to use

Webpack has enabled this feature by default, no other configuration is required.

babel

Why

  • Babel inserts helper code for every file it compiles, making it bloated!
  • Babel uses very small helper code for some public methods, eg _extend. Will be added by default to every file that needs it.
  • You can use these auxiliary codes as an independent module to avoid repeated imports.

what is

@babel/plugin-transform-runtime: disables Babel's automatic per-file runtime injection, and instead imports @babel/plugin-transform-runtimeand makes all helper code referenced from here.

how to use

  • download package
npm i @babel/plugin-transform-runtime -D
  • configuration
{
    
    
    test: /\.js$/,
        // exclude: /node_modules/, // 排除node_modules代码不编译
        include: path.resolve(__dirname, "../src"), // 也可以用包含
            use: [
                {
    
    
                    loader: "thread-loader", // 开启多进程
                    options: {
    
    
                        workers: threads, // 数量
                    },
                },
                {
    
    
                    loader: "babel-loader",
                    options: {
    
    
                        cacheDirectory: true, // 开启babel编译缓存
                        cacheCompression: false, // 缓存文件不要压缩
                        plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
                    },
                },
            ],
}

ImageMinimizer

Why

  • If many pictures are referenced in the development project, the size of the pictures will be relatively large, and the request speed will be slow in the future.
  • We can compress pictures to reduce their size.

Note: If the pictures in the project are all online links, then it is not necessary. Only static images of local projects need to be compressed.

what is

image-minimizer-webpack-plugin: A plugin for compressing images

how to use

  • download package
npm i image-minimizer-webpack-plugin imagemin -D

There are still remaining packages to download, there are two modes:

  • lossless compression
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
  • lossy compression
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D

Difference between lossy/lossless compression

  • configuration

Let's take the lossless compression configuration as an example:

const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");

optimization: {
    
    
    minimizer: [
        // 压缩图片
        new ImageMinimizerPlugin({
    
    
            minimizer: {
    
    
                implementation: ImageMinimizerPlugin.imageminGenerate,
                options: {
    
    
                    plugins: [
                        ["gifsicle", {
    
     interlaced: true }],
                        ["jpegtran", {
    
     progressive: true }],
                        ["optipng", {
    
     optimizationLevel: 5 }],
                        [
                            "svgo",
                            {
    
    
                                plugins: [
                                    "preset-default",
                                    "prefixIds",
                                    {
    
    
                                        name: "sortAttrs",
                                        params: {
    
    
                                            xmlnsOrder: "alphabetical",
                                        },
                                    },
                                ],
                            },
                        ],
                    ],
                },
            },
        }),
    ],
},
  • The following are some version issues. The wheel is not perfect yet, ignore it. When you need it, you can go to the official website to check the latest version.
Error: Error with 'src\images\1.jpeg': '"C:\Users\86176\Desktop\webpack\webpack_code\node_modules\jpegtran-bin\vendor\jpegtran.exe"'
Error with 'src\images\3.gif': spawn C:\Users\86176\Desktop\webpack\webpack_code\node_modules\optipng-bin\vendor\optipng.exe ENOENT

We need to install two files into node_modules to solve the problem. The files can be found in the courseware:

  • jpegtran.exe

needs to be copied node_modules\jpegtran-bin\vendorbelow

jpegtran official website addressopen in new window

  • optipng.exe

needs to be copied node_modules\optipng-bin\vendorbelow

OptiPNG official website address

Optimize code performance

CodeSplit

Why

  • When packaging the code, all js files will be packaged into one file, which is too large. If we only need to render the home page, we should only load the js file of the home page, and other files should not be loaded.
  • Therefore, we need to split the code of the packaged and generated files to generate multiple js files, and only load a certain js file when rendering any page, so that the loaded resources are less and the speed is faster.

what is

Code Split (Code Split) mainly does two things:

  1. Split files: Split the files generated by packaging to generate multiple js files.
  2. On-demand loading: load any file you need.

how to use

There are different ways to implement code splitting. In order to more easily reflect the differences between them, we will create new files to demonstrate

multi-entry
  • File Directory
├── public
├── src
|   ├── app.js
|   └── main.js
├── package.json
└── webpack.config.js
  • download package
npm i webpack webpack-cli html-webpack-plugin -D
  • create a new file

  • app.js

console.log("hello app");
  • main.js
console.log("hello main");
  • configuration
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    
    
  // 单入口
  // entry: './src/main.js',
  // 多入口
  entry: {
    
    
    main: "./src/main.js",
    app: "./src/app.js",
  },
  output: {
    
    
    path: path.resolve(__dirname, "./dist"),
    // [name]是webpack命名规则,使用chunk的name作为输出的文件名。
    // 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
    // chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意是前面的xxx,和文件名无关。
    // 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做main.js会发生覆盖。(实际上会直接报错的)
    filename: "js/[name].js",
    clear: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
    
    
      template: "./public/index.html",
    }),
  ],
  mode: "production",
};
  • run command
npx webpack
  • At this point in the dist directory, we can see that two js files have been output.
  • Summary: Several entries are configured, and at least several js files are output.
Extract duplicate code
  • If the same code is referenced in multiple entry files , we don't want this code to be packaged into two files, resulting in code duplication and larger size.

  • We need to extract the repeated code with multiple entries, and only package and generate a js file, which can be referenced by other files.

  • modify file

  • app.js

import {
    
     sum } from "./math";
// 两个文件均引用sum
console.log("hello app");
console.log(sum(1, 2, 3, 4));
  • main.js
import {
    
     sum } from "./math";
// 两个文件均引用sum
console.log("hello main");
console.log(sum(1, 2, 3, 4, 5));
  • math.js
export const sum = (...args) => {
    
    
  return args.reduce((p, c) => p + c, 0);
};
  • Code Splitting Configuration
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    
    
    // 单入口
    // entry: './src/main.js',
    // 多入口
    entry: {
    
    
        main: "./src/main.js",
        app: "./src/app.js",
    },
    output: {
    
    
        path: path.resolve(__dirname, "./dist"),
        // [name]是webpack命名规则,使用chunk的name作为输出的文件名。
        // 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
        // chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意是前面的xxx,和文件名无关。
        // 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做main.js会发生覆盖。(实际上会直接报错的)
        filename: "js/[name].js",
        clean: true,
    },
    plugins: [
        new HtmlWebpackPlugin({
    
    
            template: "./public/index.html",
        }),
    ],
    mode: "production",
    // ==========以下为新增代码=============
    optimization: {
    
    
        // 代码分割配置
        splitChunks: {
    
    
            chunks: "all", // 对所有模块都进行分割
            // 以下是默认值
            // minSize: 20000, // 分割代码最小的大小
            // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
            // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
            // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
            // maxInitialRequests: 30, // 入口js文件最大并行请求数量
            // enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
            // cacheGroups: { // 组,哪些模块要打包到一个组
            //   defaultVendors: { // 组名
            //     test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
            //     priority: -10, // 权重(越大越高)
            //     reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
            //   },
            //   default: { // 其他没有写的配置会使用上面的默认值
            //     minChunks: 2, // 这里的minChunks权重更大
            //     priority: -20,
            //     reuseExistingChunk: true,
            //   },
            // },
            // 修改配置
            cacheGroups: {
    
    
                // 组,哪些模块要打包到一个组
                // defaultVendors: { // 组名
                //   test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
                //   priority: -10, // 权重(越大越高)
                //   reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
                // },
                default: {
    
    
                    // 其他没有写的配置会使用上面的默认值
                    minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true,
                },
            },
        },
        // ==========以下为新增代码=============
    },
};

At this point we will find that 3 js files are generated, one of which is the extracted public module.

On-demand loading, dynamic import
  • main.js
console.log("hello main");

document.getElementById("btn").onclick = function () {
    
    
  // 动态导入 --> 实现按需加载
  // 即使只被引用了一次,也会代码分割
  import("./math.js").then(({
     
      sum }) => {
    
    
    alert(sum(1, 2, 3, 4, 5));
  });
};
  • app.js
console.log("hello app");
  • public/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, initial-scale=1.0" />
    <title>Code Split</title>
  </head>
  <body>
    <h1>hello webpack</h1>
    <button id="btn">计算</button>
  </body>
</html>

We can find that once the module is imported through the import dynamic import syntax, the module is divided by code and can be loaded on demand at the same time.

single entry
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
    
    
  entry: "./src/main.js",
  output: {
    
    
    path: path.resolve(__dirname, "./dist"),
    filename: "js/[name].js",
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
    
    
      template: "./public/index.html",
    }),
  ],
  mode: "production",
  optimization: {
    
    
    // 代码分割配置
    splitChunks: {
    
    
      chunks: "all", // 对所有模块都进行分割
  },
};
final configuration

In the end, we will use single entry + code splitting + dynamic import for configuration.

// webpack.prod.js
const os = require("os");
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");

// cpu核数
const threads = os.cpus().length;

// 获取处理样式的Loaders
const getStyleLoaders = (preProcessor) => {
    
    
  return [
    MiniCssExtractPlugin.loader,
    "css-loader",
    {
    
    
      loader: "postcss-loader",
      options: {
    
    
        postcssOptions: {
    
    
          plugins: [
            "postcss-preset-env", // 能解决大多数样式兼容性问题
          ],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
    
    
  entry: "./src/main.js",
  output: {
    
    
    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    clean: true,
  },
  module: {
    
    
    rules: [
      {
    
    
        oneOf: [
          {
    
    
            // 用来匹配 .css 结尾的文件
            test: /\.css$/,
            // use 数组里面 Loader 执行顺序是从右到左
            use: getStyleLoaders(),
          },
          {
    
    
            test: /\.less$/,
            use: getStyleLoaders("less-loader"),
          },
          {
    
    
            test: /\.s[ac]ss$/,
            use: getStyleLoaders("sass-loader"),
          },
          {
    
    
            test: /\.styl$/,
            use: getStyleLoaders("stylus-loader"),
          },
          {
    
    
            test: /\.(png|jpe?g|gif|svg)$/,
            type: "asset",
            parser: {
    
    
              dataUrlCondition: {
    
    
                maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
              },
            },
            generator: {
    
    
              // 将图片文件输出到 static/imgs 目录中
              // 将图片文件命名 [hash:8][ext][query]
              // [hash:8]: hash值取8位
              // [ext]: 使用之前的文件扩展名
              // [query]: 添加之前的query参数
              filename: "static/imgs/[hash:8][ext][query]",
            },
          },
          {
    
    
            test: /\.(ttf|woff2?)$/,
            type: "asset/resource",
            generator: {
    
    
              filename: "static/media/[hash:8][ext][query]",
            },
          },
          {
    
    
            test: /\.js$/,
            // exclude: /node_modules/, // 排除node_modules代码不编译
            include: path.resolve(__dirname, "../src"), // 也可以用包含
            use: [
              {
    
    
                loader: "thread-loader", // 开启多进程
                options: {
    
    
                  workers: threads, // 数量
                },
              },
              {
    
    
                loader: "babel-loader",
                options: {
    
    
                  cacheDirectory: true, // 开启babel编译缓存
                  cacheCompression: false, // 缓存文件不要压缩
                  plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
                },
              },
            ],
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
    
    
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
      cache: true, // 开启缓存
      // 缓存目录
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
      threads, // 开启多进程
    }),
    new HtmlWebpackPlugin({
    
    
      // 以 public/index.html 为模板创建文件
      // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
      template: path.resolve(__dirname, "../public/index.html"),
    }),
    // 提取css成单独文件
    new MiniCssExtractPlugin({
    
    
      // 定义输出文件名和目录
      filename: "static/css/main.css",
    }),
    // css压缩
    // new CssMinimizerPlugin(),
  ],
  optimization: {
    
    
    minimizer: [
      // css压缩也可以写到optimization.minimizer里面,效果一样的
      new CssMinimizerPlugin(),
      // 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
      new TerserPlugin({
    
    
        parallel: threads, // 开启多进程
      }),
      // 压缩图片
      new ImageMinimizerPlugin({
    
    
        minimizer: {
    
    
          implementation: ImageMinimizerPlugin.imageminGenerate,
          options: {
    
    
            plugins: [
              ["gifsicle", {
    
     interlaced: true }],
              ["jpegtran", {
    
     progressive: true }],
              ["optipng", {
    
     optimizationLevel: 5 }],
              [
                "svgo",
                {
    
    
                  plugins: [
                    "preset-default",
                    "prefixIds",
                    {
    
    
                      name: "sortAttrs",
                      params: {
    
    
                        xmlnsOrder: "alphabetical",
                      },
                    },
                  ],
                },
              ],
            ],
          },
        },
      }),
    ],
    // 代码分割配置
    splitChunks: {
    
    
      chunks: "all", // 对所有模块都进行分割
      // 其他内容用默认配置即可
    },
  },
  // devServer: {
    
    
  //   host: "localhost", // 启动服务器域名
  //   port: "3000", // 启动服务器端口号
  //   open: true, // 是否自动打开浏览器
  // },
  mode: "production",
  devtool: "source-map",
};
eslint dynamic import
npm i eslint-plugin-import -D
// .eslintrc.js
module.exports = {
    
    
  // 继承 Eslint 规则
  extends: ["eslint:recommended"],
  env: {
    
    
    node: true, // 启用node中全局变量
    browser: true, // 启用浏览器中全局变量
  },
  plugins: ["import"], // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
  parserOptions: {
    
    
    ecmaVersion: 6,
    sourceType: "module",
  },
  rules: {
    
    
    "no-var": 2, // 不能使用 var 定义变量
  },
};
Unified naming configuration
  output: {
    
    
    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    filename: "static/js/[name].js", // 入口文件打包输出资源命名方式
    chunkFilename: "static/js/[name].chunk.js", // 动态导入输出资源命名方式
    assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
    clean: true,
  },
{
    
    
    plugins: [
        // 提取css成单独文件
        new MiniCssExtractPlugin({
    
    
            // 定义输出文件名和目录
            filename: "static/css/[name].css",
            chunkFilename: "static/css/[name].chunk.css",
        }),
    ]
}

Preload / Prefetch

Why

  • We have already done code splitting before, and at the same time, we will use the import dynamic import syntax to load code on demand (we are also called lazy loading, such as routing lazy loading is implemented in this way).
  • But the loading speed is not good enough. For example, the resource is loaded when the user clicks the button. If the resource is large in size, the user will feel the obvious lagging effect.
  • We want to load the resources that need to be used later when the browser is idle. We need to use Preloadthe or Prefetchtechnique.

what is

  • Preload: Tells the browser to load the resource immediately.
  • Prefetch: Tells the browser to start loading resources only when it is idle.

What they have in common:

  • Both will only load resources and not execute them.
  • Both have caches.

They differ:

  • PreloadLoading priority is high, Prefetchloading priority is low.
  • PreloadOnly the resources needed for the current page can be loaded, Prefetchthe current page resources can be loaded, and the resources needed for the next page can also be loaded.

Summarize:

  • Resources with high priority on the current page are Preloadloaded with .
  • The resources to be used by the next page Prefetchare loaded with .

Their problem: poor compatibility.

  • We can go to the Can I Useopen website to check API compatibility issues.
  • PreloadPrefetchCompatibility is better.

how to use

npm i @vue/preload-webpack-plugin -D
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
{
    
    
    plugins: [
        new PreloadWebpackPlugin({
    
    
            rel: "preload", // preload兼容性更好
            as: "script",
            // rel: 'prefetch' // prefetch兼容性更差
        }),
    ]
}

Network Cache

Why

  • In future development, we will use caching to optimize static resources, so that the browser can read the cache when it requests the resource for the second time, and the speed is very fast.
  • But in this case, there will be a problem, because the output file name before and after is the same, both called main.js, once a new version is released in the future, because the file name has not changed, the browser will directly read the cache and will not load new resources , the project cannot be updated.
  • So we start with the file name and make sure that the file name is different before and after the update, so that we can do caching .

what is

They all generate a unique hash value.

  • fullhash (webpack4 is hash)

Every time any file is modified, the hash of all file names will change. So once any file is modified, the file cache of the entire project will be invalid.

  • chunkhash

According to different entry files (Entry), the dependency file is parsed, the corresponding chunk is constructed, and the corresponding hash value is generated. Our js and css are the same import and share a hash value.

  • contenthash

The hash value is generated according to the content of the file. Only when the content of the file changes, the hash value will change. All file hash values ​​are unique and unique.

how to use

{
    
    
    output: {
    
    
        path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
            // [contenthash:8]使用contenthash,取8位长度
            filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
                chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
                    assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
                        clean: true,
    },
        plugins: [
            new MiniCssExtractPlugin({
    
    
                // 定义输出文件名和目录
                filename: "static/css/[name].[contenthash:8].css",
                chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
            }),
        ]
}

Core-js

Why

  • In the past we used babel for compatibility processing of js code, which used @babel/preset-env smart presets to handle compatibility issues.
  • It can compile and transform some syntax of ES6, such as arrow function, dot operator, etc. But if it is an async function, a promise object, some methods (includes) of an array, etc., it cannot handle it.
  • So at this time, our js code still has compatibility problems. Once it encounters a low-version browser, it will directly report an error. So we want to completely solve the js compatibility problem

what is

core-jsIt is specially used for ES6 and above API polyfill.

polyfillThe translation is called shim/patch. It is to use a piece of code provided by the community to allow us to use this new feature on browsers that are not compatible with certain new features.

how to use

Eslint Compatible
  • Modify main.js
// 添加promise代码
const promise = Promise.resolve();
promise.then(() => {
    
    
  console.log("hello promise");
});

At this time, Eslint will report an error to Promise.

  • download package
npm i @babel/eslint-parser -D
  • .eslintrc.js
module.exports = {
    
    
  // 继承 Eslint 规则
  extends: ["eslint:recommended"],
  parser: "@babel/eslint-parser", // 支持最新的最终 ECMAScript 标准
  env: {
    
    
    node: true, // 启用node中全局变量
    browser: true, // 启用浏览器中全局变量
  },
  plugins: ["import"], // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
  parserOptions: {
    
    
    ecmaVersion: 6, // es6
    sourceType: "module", // es module
  },
  rules: {
    
    
    "no-var": 2, // 不能使用 var 定义变量
  },
};

Observing the packaged output js file at this time, we found that the Promise syntax does not compile and convert, so we need to use core-jsto do it polyfill.

js-compatible
npm i core-js
// 全部引入
import "core-js";
// ...
// 添加promise代码
const promise = Promise.resolve();
promise.then(() => {
    
    
  console.log("hello promise");
});

This import will import all compatibility codes, which is too large. We just want to introduce promises polyfill.

  • Manual import on demand
import "core-js/es/promise";
// 添加promise代码
const promise = Promise.resolve();
promise.then(() => {
    
    
  console.log("hello promise");
});
  • Automatic import on demand

  • babel.config.js

module.exports = {
    
    
  // 智能预设:能够编译ES6语法
  presets: [
    [
      "@babel/preset-env",
      // 按需加载core-js的polyfill
      {
    
     useBuiltIns: "usage", corejs: {
    
     version: "3", proposals: true } },
    ],
  ],
};

At this point, it will automatically load the polyfillcorresponding .

PWA

Why

  • When developing a Web App project, once the project is offline from the network, it cannot be accessed.
  • We want to give projects an offline experience.

what is

  • Progressive web application (progressive web application - PWA): It is a technology that can provide a Web App experience similar to a native app (native application program).
  • One of the most important is that the application can continue to function when offline (offline).
  • Internally implemented through Service Workers technology.

how to use

npm i workbox-webpack-plugin -D
npm i serve -g
const WorkboxPlugin = require("workbox-webpack-plugin");
{
    
    
    plugins: [
        new WorkboxPlugin.GenerateSW({
    
    
            // 这些选项帮助快速启用 ServiceWorkers
            // 不允许遗留任何“旧的” ServiceWorkers
            clientsClaim: true,
            skipWaiting: true,
        }),
    ]
}
  • main.js
if ("serviceWorker" in navigator) {
    
    
  window.addEventListener("load", () => {
    
    
    navigator.serviceWorker
      .register("/service-worker.js")
      .then((registration) => {
    
    
        console.log("SW registered: ", registration);
      })
      .catch((registrationError) => {
    
    
        console.log("SW registration failed: ", registrationError);
      });
  });
}

Summarize

We optimized webpack and code from 4 angles:

1. Improve development experience

  • Use to Source Mapenable more accurate error prompts when code errors are reported during development or online.

2. Improve webpack to increase the speed of packaging and building

  • Use to HotModuleReplacementrecompile , package and update only the changed code during development, and use the cache for the unchanged code, so that the update speed is faster.
  • OneOfUse so that once the resource file is processed by a loader, it will not continue to traverse, and the packaging speed will be faster.
  • Use Include/ExcludeExclude or only detect certain files to process fewer files faster.
  • Use to cache the results processed Cacheby eslint and babel to make the second packaging faster.
  • Use Theadmultiprocessing to handle eslint and babel tasks, which is faster. (It should be noted that there is overhead in process startup communication, and it will be effective when using more code processing)

3. Reduce code size

  • Use to Tree Shakingeliminate redundant code that is not used, making the code smaller.
  • Use @babel/plugin-transform-runtimeplugins to process babel, allowing auxiliary code to be imported from it, instead of generating auxiliary code for each file, so that the size is smaller.
  • Use Image Minimizerto compress the pictures in the project, the size is smaller, and the request speed is faster. (It should be noted that if the pictures in the project are all online links, then it is not necessary. Only the static pictures of the local project need to be compressed.)

4. Optimize code running performance

  • Use Code Splitto split the code into multiple js files, so that the size of a single file is smaller, and the js is loaded in parallel faster. And use the import dynamic import syntax to load on-demand, so that the resource is loaded when it is needed, and the resource is not loaded when it is not in use.
  • Use Preload / Prefetchto load the code in advance, so that it can be used directly when it is needed in the future, so that the user experience is better.
  • Use Network Cachecan better name the output resource file, which is easy to cache in the future, so that the user experience is better.
  • Use to perform compatibility processing Core-json js, so that our code can run in low-version browsers.
  • Using PWAmakes the code accessible offline, improving the user experience.

おすすめ

転載: blog.csdn.net/lonelysnowman/article/details/128734397