webpack5实战全解析-优化篇

2,646 阅读10分钟

前言

上篇文章webpack5实战全解析-基础篇已经实现了一个简单的 webpack 打包配置,可以进行本地开发和产出生产前端资源包。但是这还远远不满足我们发布生产上线的条件,可以优化的空间还很大,所以本篇文章主要讲解对打包配置进行优化。

1、打包速度优化

1.1、打包速度分析

在我们对 webpack 进行优化前,要知道我们 webpack 工作的状态和结果怎么样。先对打包的速度经行分析,我们使用speed-measure-webpack-plugin插件可以获取到每个插件和loader插件的耗时。

  • 安装插件
npm install speed-measure-webpack-plugin -D
  • webpack.prod.js配置进行修改:
// webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
// 速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()

const config = merge(common, {
  mode: 'production'
})

module.exports = (env, argv) => {
  return process.env.IS_SPEED === 'TRUE' ? smp.wrap(config) : config
}
  • 增加一条npm script命令,通过cross-env增加一个IS_SPEED环境变量,当IS_SPEED=TRUE时,执行速度分析插件:
{
  "scripts": {
    "speed": "cross-env IS_SPEED=TRUE npm run build"
  },
}
  • 执行npm run speed,控制台会打印出打包总的耗时和各个plugin、loader的运行耗时。

webpack_optimize_1.png

1.2、配置缓存

使用缓存是我们优化 webpack 热更新和打包速度的重要手段之一,webapck5 之前我们做的最多的就是使用cache-loader还有一些 loader 自带的缓存功能。在 webpack5 更新后在缓存处理方面带来了两个重大的变更:长期缓存和持久化缓存。

1.3、长期缓存

确定的 Chunk、模块 ID 和导出名称,新增了长期缓存的算法。这些算法在生产模式下是默认启用的。当使用 [contenthash] 时,Webpack 5 将使用真正的文件内容哈希值。之前它 "只" 使用内部结构的哈希值。 当只有注释被修改或变量被重命名时,这对长期缓存会有积极影响。这些变化在压缩后是不可见的。

1.4、持久化缓存

webpack5 有一个文件系统缓存。它是可选的,可以通过cache选项配置启用:

  • 在 webpack.common.js 里面增加配置
// webpack.common.js
module.exports = {
  cache: {
    // 将缓存类型设置为文件系统
    type: 'filesystem',
    buildDependencies: {
      // 推荐在 webpack 配置中设置 cache.buildDependencies.config: [__filename] 来获取最新配置以及所有依赖项
      config: [__filename]
    }
  }
}
  • 缓存将默认存储在 node_modules/.cache/webpack,我们再执行npm run dev,和npm run build操作,此时node_modules/.cache/webpack文件夹下多了开发环境和打包环境下面的缓存文件。

webpack_optimize_3.png

1.5、loader 缓存

  • babel-loader开启缓存
// webpack.common.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader?cacheDirectory=true'
        }
      }
    ]
  }
}
  • vue-loader使用cache-loader
npm install cache-loader -D
// webpack.common.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          // 指定缓存文件存放地址
          cacheDirectory: pathResolve('node_modules/.cache/vue-loader'),
          // 使用cache-loader
          cacheIdentifier: 'cache-loader:{version} {process.env.NODE_ENV}'
        }
      }
    ]
  }
}

1.6、缩小构建目标范围

在 loader 的工作过程中,我们可以让其精准的找到要处理的文件和目录,避免了识别处理无关文件而去浪费时间。我们可以在 loader 的 rule 中去配置excludeinclude

  • exclude:排除所有符合条件的模块。
  • include:引入符合以下任何条件的模块。

示例:对vue-loader使用

// webpack.common.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        include: [resolve('src')],
        exclude: /node_modules/,
        options: {
          // 指定缓存文件存放地址
          cacheDirectory: pathResolve('node_modules/.cache/vue-loader'),
          // 使用cache-loader
          cacheIdentifier: 'cache-loader:{version} {process.env.NODE_ENV}'
        }
      }
    ]
  }
}

1.7、oneOf可选用法

  • 我们可以使用oneOf来提高 loader 的匹配效率,匹配到一个 loader 后,后面的就不会再继续匹配了。我们对 rules 进行改造

webpack_optimize_4.png

  • 如果你只想在某些 Vue 组件中使用CSS Modules,你可以使用oneOf规则并在resourceQuery字符串中检查 module 字符串

webpack_optimize_5.png

1.8、多进程打包

官方推荐我们可以在比较耗时的 loader 操作中使用thread-loader。使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的worker池中运行。

  • 安装loader
npm install thread-loader -D
  • 我们对babel-loaderts-loader配置使用
// webpack.common.js
module.exports = {
  module: {
    rules: [
      // 转义js文件
      {
        test: /\.m?js$/,
        include: [resolve('src')],
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              // 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
              workers: 3
            }
          },
          'babel-loader?cacheDirectory=true'
        ]
      },
      // 转义tsx文件
      {
        test: /\.tsx?$/,
        include: [resolve('src')],
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: 3
            }
          },
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true,
              happyPackMode: true,
              appendTsSuffixTo: [/\.vue$/]
            }
          }
        ]
      }
    ]
  }
}

1.9、runtimeChunk

它的作用是将包含 chunks 映射关系的 list 单独从 main.js 里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以你每次改动都会影响它,如果不将它提取出来的话,等于main.js 每次都会改变。缓存就失效了。单独抽离 runtimeChunk 之后,每次打包都会生成一个 runtime~main.xxx.js。

  • 我们的组件懒加载打包出来的就是 runtime 代码,我们在项目里新建 layout 组件,并使用路由懒加载。不配置runtimeChunk进行打包操作,再对 layout 进行修改后打包,此时发现 layout.js 和 main.js 文件的 hash 都已发生变化。

webpack_optimize_26.png

webpack_optimize_27.png

  • 再配置runtimeChunk,只需将optimization.runtimeChunk为 true 就可以了
// webpack.prod.js
const config = merge(common, {
  mode: 'production',
  optimization: {
    runtimeChunk: true, // 选项将 runtime 代码拆分为一个单独的 chunk
  }
})
  • 在重复上面的操作,现在打包后会多出 runtime~main.xxx.js 文件,但前后对比 main.js 文件的 hash 值就没有改变了。

webpack_optimize_28.png

webpack_optimize_29.png

我们发现打包生成的 runtime.js非常的小,Gzip 之后一般只有几 kb,但这个文件又经常会改变,我们每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,所以建议不要将它单独拆包,而是将它内联到我们的 index.html 之中(index.html 本来每次打包都会变)。

这里选用了script-ext-html-webpack-plugin,主要是因为它还支持preloadprefetch,正好需要就不想再多引用一个插件了。

const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

// 注意一定要在HtmlWebpackPlugin之后引用
// inline 的name 和你 runtimeChunk 的 name保持一致
new ScriptExtHtmlWebpackPlugin({
  inline: /runtime~main\..*\.js$/
})
  • 此时再打包我们的 runtime~main.xxx.js 代码就被内联到 index.html 里面了

webpack_optimize_30.png

2、打包体积优化

2.1、打包体积分析

对打包速度分析和优化完后,我们再来看看我们打包出来产物的体积。使用webpack-bundle-analyzer可以查看我们打包出来文件的详情:

  • 安装 plugin
npm install webpack-bundle-analyzer -D
  • 对 webpack.prod.js 配置进行修改
// webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
// 速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()
// 体积分析插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

const config = merge(common, {
  mode: 'production'
})

// 按环境变量来决定是否启用打包体积分析插件
if (process.env.IS_ANALYZER === 'TRUE') {
  config.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = (env, argv) => {
  return process.env.IS_SPEED === 'TRUE' ? smp.wrap(config) : config
}
  • 增加一条npm script命令,通过cross-env增加一个IS_ANALYZER环境变量,当IS_ANALYZER=TRUE时,执行速度分析插件
{
  "scripts": {
    "analyzer": "cross-env IS_ANALYZER=TRUE npm run build"
  },
}
  • 执行npm run analyzer,运行成功后会在浏览器自动打开一个窗口展示打包出来文件的信息

webpack_optimize_2.png

2.2、代码压缩

2.2.1、js压缩

webpack v5 开箱即带有最新版本的terser-webpack-plugin。如果你使用的是 webpack v5 或更高版本,同时希望自定义配置,那么仍需要安装terser-webpack-plugin

  • 安装plugin
npm install terser-webpack-plugin -D
  • 在配置文件中配置,我们自定义除去代码中的console.log()
// webpack.prod.js
const config = merge(common, {
  mode: 'production',
  optimization: {
    runtimeChunk: true,
    moduleIds: 'deterministic',
    usedExports: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true // 删除console.log
          }
        }
      })
    ]
  }
})

2.2.2、css压缩

webpack5 之前我们会使用extract-text-webpack-plugin插件对我们的 css 进行压缩和提取操作。 但webpack v5 的新特性为我们带来了 MiniCssExtractPlugin,它会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。

  • 安装 plugin
npm install mini-css-extract-plugin -D
  • 配置 loader

我们区分环境,对于生产环境使用MiniCssExtractPlugin,在样式文件被css-loader处理后再使用MiniCssExtractPlugin.loader处理

// webpack.common.js
const isDev = process.env.NODE_ENV === 'development'

// 生产环境启用 MiniCssExtractPlugin
const styleLoader = isDev
  ? {
      loader: 'vue-style-loader',
      options: {
        sourceMap: false,
        shadowMode: false
      }
    }
  : MiniCssExtractPlugin.loader

module.exports = {
  module: {
    rules: [
      {
        oneOf: [
          // 处理.css文件
          {
            test: /\.css$/,
            include: [
              resolve('src')
            ],
            oneOf: [
              // 这里匹配 `<style module>`
              {
                resourceQuery: /module/,
                use: [
                  styleLoader,
                  {
                    loader: 'css-loader',
                    options: {
                      modules: true, // 启用 CSS 模块
                      localIdentName: '[local]_[hash:base64:5]'
                    }
                  },
                  {
                    loader: 'postcss-loader',
                    options: {
                      sourceMap: false // 禁用生成 SourceMap
                    }
                  }
                ]
              },
              // 这里匹配普通的 `<style>` 或 `<style scoped>`
              {
                use: [
                  styleLoader,
                  {
                    loader: 'css-loader',
                    options: {
                      sourceMap: false,
                      importLoaders: 1 // 设置在 css-loader 前应用的 loader 数量
                    }
                  },
                  {
                    loader: 'postcss-loader',
                    options: {
                      sourceMap: false
                    }
                  }
                ]
              }
            ]
          },
          // 处理.scss文件
          {
            test: /\.(sa|sc)ss$/,
            include: [resolve('src')],
            exclude: /node_modules/,
            use: [
              styleLoader,
              {
                loader: 'css-loader',
                options: {
                  sourceMap: false,
                  importLoaders: 2
                }
              },
              {
                loader: 'postcss-loader',
                options: {
                  sourceMap: false
                }
              },
              {
                loader: 'sass-loader',
                options: {
                  sourceMap: false
                }
              }
            ]
          }
        ]
      }
    ]
  }
}
  • 配置 plugin
// webpack.prod.js

// 提取css
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const config = merge(common, {
  mode: 'production',
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css', // 启动长期缓存。根据需要添加 [name]
      chunkFilename: 'static/css/[id].[contenthash:8].css'
    })
  ]
})
  • 执行npm run build打包成功后,在dist/static目录下发现,多了一个包含main.*.css文件的 css 文件夹。

webpack_optimize_8.png

npm install css-minimizer-webpack-plugin -D
// 压缩css
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")

const config = merge(common, {
  mode: 'production',
  optimization: {
    minimizer: [
      new CssMinimizerPlugin({
        minimizerOptions: {
          preset: [
            'default',
            {
              discardComments: { removeAll: true } // 移除所有注释(包括以 /*! 开头的注释)
            }
          ]
        }
      })
    ]
  }
})
  • 此时再执行npm run build,原先的main.*.css文件中的代码就被压缩了

webpack_optimize_9.png

2.3、SplitChunks分包

使用SplitChunks可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk,避免一些模块多处引入出现重复打包到多个文件里的情况。SplitChunks在生产环境下默认开启,我们先使用默认配置进行打包分析,执行npm run analyzer

为了效果明显这里还有以下某些演示场景使用实际上线的项目代码进行操作!!!

webpack_optimize_6.png

  • 我们再根据自己项目实际情况对其配置进行修改
// webpack.prod.js
const config = merge(common, {
  mode: 'production',
  optimization: {
    runtimeChunk: true, // 选项将 runtime 代码拆分为一个单独的 chunk
    moduleIds: 'deterministic', // 是否添加任何新的本地依赖,对于前后两次构建,vendor hash 都应该保持一致
    usedExports: true,
    // 将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中
    splitChunks: {
      chunks: 'all', // 这表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 all,async 和 initial
      minSize: 20000, // 生成 chunk 的最小体积
      maxAsyncRequests: 6, // 按需加载时的最大并行请求数
      maxInitialRequests: 6, // 入口点的最大并行请求数
      cacheGroups: {
        vendor: {
          name: 'chunk-vendors',
          minChunks: 2, // 拆分前必须共享模块的最小 chunks 数
          test: /[\\/]node_modules[\\/]/,
          priority: 10,
          reuseExistingChunk: true
        },
        fantUI: {
          name: 'chunk-fantUI',
          test: /[\\/]node_modules[\\/]_?fant-ui(.*)/,
          priority: 15,
          reuseExistingChunk: true
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
})
  • 再执行npm run analyzer,我们根据priority优先级把 UI组件库fant-ui单独提取出来,echarts因为体积过大也被提取出来,其余的node_modules依赖被打到一个bundle里面,一些项目公用的模块被打到chunk-common bundle文件里面;这样项目的入口文件main.js的体积就大大缩小了,使得项目首屏的加载速度提升。

webpack_optimize_7.png

2.4、CDN引入第三方模块

对于项目里面的一些第三方模块我们不想跟随项目进行打包部署,减少打包后的体积,减轻部署服务器的压力。我们可以采用 CDN 引入的方式来处理。例如我们在上面SplitChunks分包后可以看出,echartsaliyun-oss-sdk这两个第三方包的体积都比较大,我们就可以使用CDN来处理。

  • webpack 的externals配置选项提供了「从输出的 bundle 中排除依赖」的方法,CDN这里可以使用jsdelivr

  • 修改 webpack.common.js

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      title: 'retail-admin',
      filename: 'index.html',
      template: 'index.html',
      inject: true,
      scriptLoading: 'defer',
      cdn: [
        'https://cdn.jsdelivr.net/npm/echarts@5.2.0/dist/echarts.min.js', // jsdelivr CDN引入echarts
        'https://cdn.jsdelivr.net/npm/ali-oss@6.16.0/dist/aliyun-oss-sdk.min.js' // jsdelivr CDN引入aliyun-oss-sdk
      ]
    })
  ],
  externals: {
    echarts: 'echarts',
    'ali-oss': 'OSS'
  }
}
  • 修改index.html
<!DOCTYPE html>
<html lang="zh">
  <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><%= htmlWebpackPlugin.options.title %></title>
    <!-- 引入cdnJS -->
    <% for(var js of htmlWebpackPlugin.options.cdn) { %>
      <script defer src="<%=js%>"></script>
    <% } %>
  </script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
  • 再执行npm run analyzer,就可以看到echartsaliyun-oss-sdk都已经从我们的项目包里剔除出去了。

webpack_optimize_19.png

2.4.1、CDN挂了

突然有一天测试反馈页面打不开了,然后查找原因发现是CDN引入的文件无法加载!!!

webpack_optimize_21.png

echarts官网都中招了,打开后只有 html 一些结构展示。打开控制台发现一堆文件夹加载异常的报错,网站的第三方包基本都是引用的jsdelivr的链接

webpack_optimize_20.png

因为公司没有自己的CDN,为了防止以后再出现这个问题而产生生产事故让自己背锅,所以就取消了使用外部CDN引入。。。

2.5、图片压缩

项目中使用图片文件较多的话对图片进行压缩可以大大减少项目包的体积,webpack 可以使用image-webpack-loader进行图片压缩。

  • 安装image-webpack-loader,此处要用cnpm去进行安装否则运行会出现报错(坑)
npm install cnpm -g --registry=https://registry.npm.taobao.org
cnpm install image-webpack-loader -D
  • 修改对图片文件处理的配置
// 处理图片文件,Webpack5.0新增资源模块(asset module)处理静态资源
{
  test: /\.(png|jpe?g|gif|webp)(\?.*)?$/,
  include: [resolve('src')],
  exclude: /node_modules/,
  // 图片压缩
  use: [
    // image-webpack-loader需要用cnpm安装否则容易报错
    {
      loader: 'image-webpack-loader',
      options: {
        mozjpeg: {
          progressive: true
        },
        // optipng.enabled: false will disable optipng
        optipng: {
          enabled: false
        },
        pngquant: {
          quality: [0.65, 0.9],
          speed: 4
        },
        gifsicle: {
          interlaced: false
        },
        // the webp option will enable WEBP
        webp: {
          quality: 75
        }
      }
    }
  ],
  type: 'asset',
  generator: {
    // 输出文件位置以及文件名
    filename: 'images/[name].[hash:8].[ext]'
  },
  parser: {
    dataUrlCondition: {
      maxSize: 10 * 1024 // 超过10kb不转base64
    }
  }
}

2.5.1、熊猫压图

由于使用image-webpack-loader,安装不友好且容易报错,在CI/CD环境打包时只能使用npm安装依赖包。我还是比较推荐使用 tinypng 一个在线压图的网站手动压缩项目图片。

  • 项目里的图片原本没有经过压缩,打出来的包,images 文件夹有很多图片

webpack_optimize_16.png

  • 将项目里的图片使用tinypng进行压缩,可以看到压缩后的图片体积能能减少 50% 以上

webpack_optimize_17.png

  • 在对项目进行打包,此时images文件夹就只剩下几张图片了,因为之前配置了图片不超过 10kb 转 base64;项目包的大小也大大缩减了。

webpack_optimize_18.png

2.6、开启 Gzip

可以减小文件体积,传输速度更快。Gzip是节省带宽和加快网站速度的有效方法。

npm install compression-webpack-plugin -D
  • 增加一条npm script命令,通过cross-env增加一个IS_GZIP环境变量,当IS_GZIP=TRUE时,执行compression-webpack-plugin插件:
{
  "scripts": {
    "build:gzip": "cross-env IS_GZIP=TRUE npm run build"
  }
}
  • 增加 webpack 配置
// webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
// 速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()
// 体积分析插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
// 开启 Gzip
const CompressionWebpackPlugin = require('compression-webpack-plugin')

const config = merge(common, {
  // ...
})

// 按环境变量IS_ANALYZER来决定是否启用打包体积分析插件
if (process.env.IS_ANALYZER === 'TRUE') {
  config.plugins.push(new BundleAnalyzerPlugin())
}

// 按环境变量IS_GZIP来决定是否启用Gzip
if (process.env.IS_GZIP === 'TRUE') {
  config.plugins.push(new CompressionWebpackPlugin({
    filename: '[path][base].gz', // 输出目标文件名
    algorithm: 'gzip', // 压缩格式
    test: new RegExp('\\.(js|css)$'), // 要处理的文件正则
    threshold: 0, // 仅处理大于此大小的文件(以字节为单位)
    minRatio: 0.8, // 仅处理压缩比此比率更好的文件(minRatio = 压缩大小/原始大小)
    deleteOriginalAssets: false // 是否删除原有文件
  }))
}

module.exports = (env, argv) => {
  return process.env.IS_SPEED === 'TRUE' ? smp.wrap(config) : config
}
  • 再执行·npm run build:gzip此时打出来的包多出了.gz格式 的文件,且大小比源文件压缩了很多。

webpack_optimize_22.png

  • 这里还需要运维去处理开启web服务器Gzip支持...

  • 发布上线后我们查看控制面板的 Network 文件请求的 Response headres 显示Content-Encoding:gzip,说明启用成功了

webpack_optimize_23.png

  • 我们对比 Network 里面文件加载大小和本地打包出来文件的大小,发现整体上体积被大大的压缩了(ps:忽略文件的哈希值不一样,一个是本地打包一个是线上打包出来的)。

webpack_optimize_24.png

webpack_optimize_25.png

3、打包体验优化

3.1、处理开发环境端口占用

我们在对多个前端项目进行开发时,本地可能会开启多个 web服务,此时很有可能会出现端口被占用的情况,执行npm run dev开启项目热更新服务,此时端口设置为 9000,然后再开一个终端窗口再执行npm run dev。此时控制台报错 端口地址已经被使用。

webpack_optimize_10.png

  • 此时我们还得手动去修改配置再运行,不太友好。我们可以通过中间件portfinder去设置默认端口和获取到未被占用端口再对 webpack 进行设置

  • 安装 portfinder

npm install portfinder -D
  • 修改 webpack 配置
// webpack.dev.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const { resolve } = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const utils = require('./utils')

function pathResolve(dir) {
  return resolve(process.cwd(), '.', dir)
}

const devWebpackConfig  = merge(common, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: {
    // ...
  }
})


// 处理端口被占用
module.exports = new Promise((resolve, reject) => {
  // 设置默认端口为8080
  portfinder.basePort = process.env.PORT || 8080
  // 获取没有被占用的端口
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      process.env.PORT = port
      // 设置启动热更新服务的端口
      devWebpackConfig.devServer.port = port

      // 启动后根据实际端口再用 FriendlyErrorsWebpackPlugin 插件在终端进行信息展示
      devWebpackConfig.plugins.push(
        new FriendlyErrorsWebpackPlugin({
          compilationSuccessInfo: {
            messages: [
              `Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`
            ]
          },
          // 异常处理,使用 node-notifier 进行原生通知
          onErrors: utils.createNotifierCallback()
        })
      )

      resolve(devWebpackConfig)
    }
  })
})
  • 安装node-notifier,使用 Node.js 发送跨平台原生通知。
npm install node-notifier -D
  • 新建 utils.js 文件
'use strict'
const path = require('path')
const packageConfig = require('../package.json')

exports.createNotifierCallback = () => {
  const notifier = require('node-notifier')

  return (severity, errors) => {
    if (severity !== 'error') return

    const error = errors[0]
    const filename = error.file && error.file.split('!').pop()

    // 跨平台原生消息通知
    notifier.notify({
      title: packageConfig.name,
      message: severity + ': ' + error.name,
      subtitle: filename || '',
      // 设置通知显示图标
      icon: path.join(__dirname, '../public/favicon.ico')
    })
  }
}
  • 此时我们在开发环境编译报错后,电脑就会出现报错信息弹框,且消息通知栏也有弹框信息。

3.2、优化生产打包终端提示

在前面的配置中使用了ProgressPlugin插件用来对 webpack 运行进度信息进行展示,但是日常开发中需用到生产打包的场景很少,官方文档中也说了ProgressPlugin可能不会为快速构建提供太多价值。我们对配置进行修改,只在开发环境进行进度提示,生产环境打包大多用在CI/CD时才会执行,我们只需关注打包是否成功。

webpack_optimize_11.png

  • 安装ora ,优雅的终端微调器插件
npm install ora@1.2.0 -D
  • 安装rimraf用于替代之前的output.clean清除 dist 目录的功能
npm install rimraf -D
  • 安装chalk,使控制台输出不再单调,可以添加添加文字背景、改变字体颜色等
npm install chalk@2.4.2 -D
  • 新建 build.js 文件,更改npm script的 build 命令。
{
  "scripts": {
    "build": "cross-env NODE_ENV=production node ./webpack/build.js"
  }
}
// build.js
'use strict'

process.env.NODE_ENV = 'production'

const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const webpackConfig = require('./webpack.prod')()

// 开启优雅的终端微调器 ora
const spinner = ora('building for production...')
spinner.start()

// rimraf 清空指定文件夹,回调执行 webpack 进行打包
rm(path.join(path.resolve(__dirname, '../dist'), 'static'), err => {
  if (err) throw err
  // 执行 webpack 打包
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    // 控制台打印 webpack 信息输出(console.log()是 process.stdout.write 带有格式化输出的调用)
    process.stdout.write(
      // 官方提醒在使用 Node.js API 时,需要将统计配置项传递给 stats.toString()
      stats.toString({
        assets: false, // 告知 stats 是否展示资源信息
        colors: true, // 告知 stats 是否输出不同的颜色
        modules: false, // 告知 stats 是否添加关于构建模块的信息
        children: false, // 告知 stats 是否添加关于子模块的信息
        entrypoints: false, // 告知 stats 是否展示入口文件与对应的文件 bundles
        chunks: false, // 告知 stats 是否添加关于 chunk 的信息
        chunkModules: false // 告知 stats 是否添加关于已构建模块和关于 chunk 的信息
      }) + '\n\n'
    )

    // 打包异常退出进程
    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    // 正常打包打印信息
    console.log(chalk.cyan('  Build complete.\n'))
    console.log(
      chalk.yellow(
        '  Tip: built files are meant to be served over an HTTP server.\n' +
          "  Opening index.html over file:// won't work.\n"
      )
    )
  })
})
  • 再执行 npm run build 命令,下图分别为打包中和打包结束的控制台输出信息

webpack_optimize_12.png

webpack_optimize_13.png

3.3、检查打包环境node版本

webpack 运行在 Node.js 中,而且运行 webpack 5 的 Node.js 最低版本是10.13.0 (LTS)。在低版本的 Node.js 环境进行 webpack 打包,由于部分 Node.js API 不兼容会出现打包异常,所以我们在打包前进行版本检查是很有必要的!防止打出来的包在生产上不可用,产生生产事故。

  • package.json 配置engines
{
  "engines": {
    "node": ">= 10.13.0",
    "npm": ">= 3.0.0"
  }
}
  • 安装semver,语义化版本(Semantic Versioning)规范的一个实现
npm install semver -D
  • 安装 shelljs,Node.js 下的脚本语言解析器
npm install shelljs -D
  • 新建check-versions.js文件
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')

// 创建 Node 同步进程,运行 cmd 命令
function exec(cmd) {
  return require('child_process').execSync(cmd).toString().trim()
}

const versionRequirements = [
  {
    name: 'node',
    // semver.clean('  =v1.2.3   ') // '1.2.3'
    currentVersion: semver.clean(process.version),
    versionRequirement: packageConfig.engines.node
  }
]

// 检查控制台是否可以运行 npm 开头的命令
if (shell.which('npm')) {
  versionRequirements.push({
    name: 'npm',
    // 创建 Node 同步进程,运行 npm --version 命令
    currentVersion: exec('npm --version'),
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function () {
  const warnings = []

  for (let i = 0; i < versionRequirements.length; i++) {
    const mod = versionRequirements[i]

    // semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3') // true
    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
      warnings.push(
        mod.name +
          ': ' +
          chalk.red(mod.currentVersion) +
          ' should be ' +
          chalk.green(mod.versionRequirement)
      )
    }
  }

  // 控制台打印异常信息
  if (warnings.length) {
    console.log('')
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()

    for (let i = 0; i < warnings.length; i++) {
      const warning = warnings[i]
      console.log('  ' + warning)
    }

    console.log()
    // 结束webpack进程
    process.exit(1)
  }
}
  • 在 build.js 中引入调用 check-versions
// build.js
'use strict'

require('./check-versions')()
  • 使用nvm修改 node 版本,使其不满足engines的要求

webpack_optimize_14.png

  • 再执行npm run build,此时控制台就会打印出版本异常信息,并终止 webpack 运行进程。

webpack_optimize_15.png

4、完结

代码地址

参考文献:

webpack官网

vuejs-templates/webpack

手摸手,带你用合理的姿势使用webpack4(下)

往期文章:

webpack5实战全解析-基础篇

使用nvm管理node版本,一条龙解决前端开发环境配置