Webpack5学习笔记(高级篇)

宦飞
2023-12-01

SourceMap

为什么

当代码经编译后在浏览器中运行时,若发生错误,定位不到源代码的位置,无法立即找到错误

是什么

是一个用来生成源代码与构建后代码一一映射的方案

怎么用

在webpack.config.js中进行配置

  • 在开发模式下用cheap-module-source-map
{
    // ...
	mode: 'development',
    devtool: 'cheap-module-source-map'
}
  • 在生产模式下用source-map
{
    // ...
    mode: 'production',
    devtool: 'source-map'
}

HotModuleReplacement

为什么

开发时若修改了一个模块的代码,webpack默认会将所有的模块全部重新打包编译,从而导致速度很慢

是什么

在程序运行时,替换,添加或删除模块,无需重新加载整个页面

怎么用

在webpack.config.js中进行配置

devServer: {
    host: 'localhost', // 启动服务器域名
    port: '3000', // 启动服务器端口号
    open: true, // 是否打开
    // 默认为true
    hot: true
},
mode: 'development'

注意

style-loader天然具备热模块替换的功能,但是对于js代码还会刷新页面,解决方式是在main.js中进行配置

//判断是否支持热更新
if(module.hot){
  //设置热更新的模块
  module.hot.accept('./components/sub')
  module.hot.accept('./components/sum')
}

oneOf

为什么

对于项目中的文件,会逐个的去匹配webpack.config.js中的加载器,为了提高打包速度,应该在当某一项匹配成功时就停止匹配

怎么用

在webpack.config.js进行配置

module: {
    rules: [
        {
            oneOf: [
                {
                    test: /\.css$/,
                    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|svg|jpe?g|gif|webp)$/,
                    type: 'asset',
                    parser: {
                        dataUrlCondition: {
                            maxSize: 2 * 1024,
                        }
                    },
                    generator: {
                        filename: 'static/images/[hash:10][ext][query]'
                    }
                },
                {
                    test: /\.(ttf|woff2?|mp3|avi)$/,
                    type: 'asset/resource',
                    generator: {
                        filename: 'static/font/[hash:10][ext][query]'
                    }
                },
                {
                    test: /\.js$/,
                    exclude: /node_modules/,
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }]
        }
    ]
},

include和exclude

是什么

用于处理指定文件下的js文件,include指包含 exclude指排除

怎么用

esLint

new ESLintPlugin({
    //指定检查哪个目录下的文件
    context: path.resolve(__dirname, '../src')
    //用于排除,注意,此配置项中没有include
    exclude: path.resolve(__dirname, '../src/components/'),
}),

babel

{
    test: /\.js$/,
    // exclude用于排除文件 include用户包含文件  此配置项中include和exclude均可以用
    exclude: /node_modules/,
    loader: 'babel-loader',
    options: {
        presets: ['@babel/preset-env']
    }
}

Eslint和Babel的缓存

  • 开始babel缓存
{
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
    options: {
        // presets: ['@babel/preset-env']
        // 开启babel缓存
        cacheDirectory: true,
        // 关闭缓存文件压缩
        cacheCompression: false
    }
}
  • 开始eslint缓存
 new ESLintPlugin({
    // exclude: path.resolve(__dirname, '../src/components/'),
    context: path.resolve(__dirname, '../src'),
    // 开启缓存
    cache: true,
    // 指定缓存目录
    cacheLocation: path.resolve(__dirname,'../node_modules/.cache/eslintCache')
}),

多进程打包

  1. 获取cpu的核数
const os = require('os')
const coreNum = os.cpus().length;
  1. 使用命令npm install thread-loader -D安装包
  2. 对babel进行多进程配置
{
    test: /\.js$/,
    exclude: /node_modules/,
    use: [
        {
            // 开启多进程,对babel做处理
            loader: 'thread-loader',
            options: {
                // 进程数量
                works: coreNum
            }
        },
        {
            loader: 'babel-loader',
            options: {
                // presets: ['@babel/preset-env']
                // 开启babel缓存
                cacheDirectory: true,
                // 关闭缓存文件压缩
                cacheCompression: false
            }
        }
    ]
}
  1. 对eslint进行多进程配置
new ESLintPlugin({
    // exclude: path.resolve(__dirname, '../src/components/'),
    context: path.resolve(__dirname, '../src'),
    // 开启缓存
    cache: true,
    // 指定缓存目录
    cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintCache'),
    // 开启多进程和进程数量
    threads:coreNum
}),
  1. 压缩js
const TerserWebpackPlugin = require('terser-webpack-plugin')
new TerserWebpackPlugin({
    // 开启多进程和进程数量
    parallel: coreNum
})

optimization

const os = require('os')
const coreNum = os.cpus().length;

const path = require('path')
const ESLintPlugin = 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 TerserWebpackPlugin = require('terser-webpack-plugin')
console.log('cpu的核数为:', coreNum)

function getStyleLoader(pre) {
    return [
        MiniCssExtractPlugin.loader,
        'css-loader',
        {
            loader: "postcss-loader",
            options: {
                postcssOptions: {
                    plugins: [
                        [
                            "postcss-preset-env",
                        ],
                    ],
                },
            }
        },
        pre].filter(Boolean)
}
module.exports = {
    entry: './src/main.js',
    output: {
        path: path.resolve(__dirname, '../bundle'),
        filename: 'main.js',
        clean: true
    },
    module: {},
    plugins: [],
    optimization: {
        // 用来存放压缩的东西
        minimizer: [
            // 压缩css代码
            new CssMinimizerPlugin(),
            new TerserWebpackPlugin({
                // 开启多进程和进程数量
                parallel: coreNum
            })
        ]
    },
    //...
}

减少代码体积

为什么

在开发时会引入第三方库,但是实际使用时只会使用该库中的一部分函数,若不进行特殊设置,则会对第三方库都进行打包,浪费空间。

怎么用

在webpack中默认开启了Tree shaking功能,无需配置

减少babel生成文件的体积

  1. 使用npm install @babel/plugin-transform-runtime -D命令安装包
  2. 配置babel
test: /\.js$/,
exclude: /node_modules/,
use: [{
    loader: 'babel-loader',
    options: {
        // presets: ['@babel/preset-env']
        // 开启babel缓存
        cacheDirectory: true,
        // 关闭缓存文件压缩
        cacheCompression: false,
        // 减少代码体积
        plugins: ["@babel/plugin-transform-runtime"]
    }
}]

压缩图片

  1. 使用npm i image-minimizer-webpack-plugin imagemin -D命令下载包
  2. 使用npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D命令下载无损压缩包
  3. 使用npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D命令下载有损压缩包
  4. 配置
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
 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",
                      },
                    },
                  ],
                },
              ],
            ],
          },
        },
      }),
    ],
  },

代码分割

多入口

  1. 文件目录
├── public
├── src
|   ├── app.js
|   └── main.js
├── package.json
└── webpack.config.js
  1. 下载包
npm i webpack webpack-cli html-webpack-plugin -D
  1. 在webpack.config.js文件中进行配置
const path = require('path')

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

module.exports = {
  entry: {
    main: './src/main.js',
    app: './src/app.js'
  },
  output: {
    path: path.resolve(__dirname, 'bundle'),
    filename: '[name].js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/index.html')
    })
  ],
  mode: 'development'
}
  1. 使用npx webpack进行打包

提取重复代码

修改webpack.config.js文件

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,
    },
  },
}

按需加载,动态导入

在导入js文件时,需要使用import函数

document.getElementById('btn').onclick = function () {
  import('./say').then(res => {
    //res.default就是方法
    res.default('Webpack')
  })
}

单入口

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,
      //   },
      // },
  },
};

若eslint不能识别import,则需要在.eslintrc.js中进行配置

  1. 使用命令npm i eslint-plugin-import -D下载包
module.exports  {
    // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
    plugins:["import"]
}

给动态导入的模块命名

  1. 在导入时
document.getElementById('btn').onclick = function () {
  import(/* webpackChunkName: 'say' */ './say').then(res => {
    res.default('Webpack')
  })
}
  1. 在webpack.config.js中进行配置
module.exports = { 
    output: {
        path: path.resolve(__dirname, 'bundle'),
        filename: '[name].js',
        chunkFilename: 'dynamic/[name].js'
      },
}

统一命名

  output: {
    path: path.resolve(__dirname, 'bundle'),
    // 入口文件打包后输出时的文件名 [name]用于设置多入口文件名称
    filename: '[name].js',
    // 使用import函数按需导入的js文件打包后输出时的文件名
    chunkFilename: 'dynamic/[name].chunk.js',
    // 图片、字体等通过type:asset处理的资源
    assetModuleFilename: "static/media/[hash:10][ext][query]"
    // 对于css文件的动态导入也使用filename 和 chunkFilename来设置
  },

preLoad和prefetch

为什么

我们前面已经做了代码分割,同时会使用 import 动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。

但是加载速度还不够好,比如:是用户点击按钮时才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果。

我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 PreloadPrefetch 技术。

是什么

  • Preload:告诉浏览器立即加载资源。

  • Prefetch:告诉浏览器在空闲时才开始加载资源。

它们共同点:

  • 都只会加载资源,并不执行。
  • 都有缓存。

它们区别:

  • Preload加载优先级高,Prefetch加载优先级低。
  • Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源。

总结:

  • 当前页面优先级高的资源用 Preload 加载。
  • 下一个页面需要使用的资源用 Prefetch 加载。

怎么用

  1. 下载包
npm i @vue/preload-webpack-plugin -D
  1. 配置webpack.config.js
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/index.html')
    }),
    new PreloadWebpackPlugin({
      // preload
      rel: "preload", // preload兼容性更好
      as: "script",

      // prefetch
      // rel: 'prefetch' // prefetch兼容性更差
    }),
  ],

Network Cache

为什么

将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快。

但是这样的话就会有一个问题, 因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。

所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了。

是什么

它们都会生成一个唯一的 hash 值。

  • fullhash(webpack4 是 hash)

每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。

  • chunkhash

根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。

  • contenthash

根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。

如何用

  output: {
    path: path.resolve(__dirname, 'bundle'),
    // 入口文件打包后输出时的文件名 [name]用于设置多入口文件名称
    filename: '[name].[contenthash:8].js',
    // 使用import函数按需导入的js文件打包后输出时的文件名
    chunkFilename: 'dynamic/[name].[contenthash:8].chunk.js',
    // 图片、字体等通过type:asset处理的资源
    assetModuleFilename: "static/media/[name].[hash][ext]"
    // 对于css文件的动态导入也使用filename 和 chunkFilename来设置
  },
  • 问题:

当我们修改 math.js 文件再重新打包的时候,因为 contenthash 原因,math.js 文件 hash 值发生了变化(这是正常的)。

但是 main.js 文件的 hash 值也发生了变化,这会导致 main.js 的缓存失效。明明我们只修改 math.js, 为什么 main.js 也会变身变化呢?

  • 原因:

    • 更新前:math.xxx.js, main.js 引用的 math.xxx.js
    • 更新后:math.yyy.js, main.js 引用的 math.yyy.js, 文件名发生了变化,间接导致 main.js 也发生了变化
  • 解决:

将 hash 值单独保管在一个 runtime 文件中。

我们最终输出三个文件:main、math、runtime。当 math 文件发送变化,变化的是 math 和 runtime 文件,main 不变。

runtime 文件只保存文件的 hash 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小。

在webpack.config.js文件中进行配置

  optimization:
  {
    runtimeChunk: {
      name: entryPoint => `runtime~${entryPoint.name}.js`,
    }
  }

Core-js

为什么

过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env 智能预设来处理兼容性问题。

它能将 ES6 的一些语法进行编译转换,比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。

所以此时我们 js 代码仍然存在兼容性问题,一旦遇到低版本浏览器会直接报错。所以我们想要将 js 兼容性问题彻底解决

是什么

core-js 是专门用来做 ES6 以及以上 API 的 polyfill

polyfill翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性。

怎么用

  1. 使用npm i core-js命令下载包

  2. 全部引入

    	import "core-js";
    
  3. 自动按需引入,在babel.config.js文件中进行配置

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

问题

若Eslint 会对 Promise 报错,则需要使用`npm i @babel/eslint-parser -D`命令下载包,然后在eslintrc.js中进行配置

```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 定义变量
  },
};
```

PWA

为什么

开发 Web App 项目,项目一旦处于网络离线情况,就没法访问了。

我们希望给项目提供离线体验。

是什么

渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。

其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。

内部通过 Service Workers 技术实现的。

怎么用

  1. 下载包npm i workbox-webpack-plugin -D
  2. 在webpack中进行配置
const WorkboxPlugin = require("workbox-webpack-plugin");
plugins:[
      new WorkboxPlugin.GenerateSW({
      // 这些选项帮助快速启用 ServiceWorkers
      // 不允许遗留任何“旧的” ServiceWorkers
      clientsClaim: true,
      skipWaiting: true,
    }),
]
  1. 在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);
      });
  });
}
  1. 使用命令npm run build运行

  2. 此时如果直接通过 VSCode 访问打包后页面,在浏览器控制台会发现 SW registration failed

    因为我们打开的访问路径是:http://127.0.0.1:5500/dist/index.html。此时页面会去请求 service-worker.js 文件,请求路径是:http://127.0.0.1:5500/service-worker.js,这样找不到会 404。

    实际 service-worker.js 文件路径是:http://127.0.0.1:5500/dist/service-worker.js

6.解决方法,使用命令npm i serve -g下载包,使用serve dist开启服务

 类似资料: