【进阶篇】还不敢动 webpack 的配置 ?

1,255 阅读10分钟

前言

【基础篇】还不敢动 webpack 的配置 ?的续文,在基础上增加如 PostCss 插件的运用、构建速度的提升、打包文件体积的优化、集成Eslint等。

如果不熟悉 webpack 基本的概念,可以先看基础篇。

实践

PostCSS

是一个用 JavaScript 工具和插件转换 CSS 代码的工具

先来讲一下最常见的如autoprefixer插件,用于自动补齐 CSS3 前缀,从而保证 CSS3 样式兼容各大浏览器。

安装 npm i postcss-loader autoprefixer -D

webpack.prod.js添加如下代码。

{
  test: /\.scss$/,
  use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'sass-loader',
+    {
+      loader: 'postcss-loader',
+      options: {
+        postcssOptions: {
+          plugins: ['autoprefixer']
+        }
+      }
+    }
+  ]
},

早期配置browsers需在 autoprefixer 的 options 进行配置,现官方推荐在package.json文件以browserslist为 key 值去配置或者新建.browserslistrc文件进行配置。

browserslist 官网🐱‍🏍

本文以package.json增加字段为例,如下所示。

+ browserslist: [
+   "last 2 Chrome versions",
+   "> 0.2%",
+   "ios 7"
+ ],

证明一下配置是否成功可以尝试用 transform 属性再进行打包,可发现打包后的文件自动添加了前缀。

还有常用的如postcss-pxtorempostcss-px-to-viewport,这我之前有一篇【自适应】px 转 rem,你还在手算么?文章通过新建postcss.config.js文件去完成 px 转 rem 的。那今天就教大家完成 px 转 vw 的配置。

autoprefixer一样,在 plugins 添加,如下所示。

{
  test: /\.scss$/,
  use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'sass-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'autoprefixer',
+            [
+              'postcss-px-to-viewport',
+              {
+                viewportWidth: 1920,  // 设计稿定的基准宽
+                unitPrecision: 5, // 转换后保留的小数位数
+                minPixelValue: 1 // 小于或等于 1px 不转换 vw
+              }
+            ]
          ]
        }
      }
    }
  ]
}

重新打包可观测到已经转为 vw 的单位。

多页面打包

多页面打包主要是配置 entry 字段以及 HtmlWebpackPlugin,多应用于 MPA(多页面应用)。当页面数量过多,需要设置多个入口及对应配置页面太耗费时间,所以使用 glob 动态获取入口文件。

代码摘自《极客时间-玩转Webpack》程柳峰大佬👍。

安装 npm i glob -D

编写函数如下,即插即用,根据项目结构更改正则即可,官网👉html-webpack-plugin

const glob = require('glob')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

const setMPA = () => {
    const entry = {};
    const htmlWebpackPlugins = [];
    const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));

    Object.keys(entryFiles)
        .map((index) => {
            const entryFile = entryFiles[index];
            const match = entryFile.match(/src\/(.*)\/index\.js/);
            const pageName = match && match[1];

            entry[pageName] = entryFile;
            htmlWebpackPlugins.push(
                new HtmlWebpackPlugin({    
                    template: path.join(__dirname, `src/${pageName}/index.html`),
                    filename: `${pageName}.html`,
                })
            );
        });

    return {
        entry,
        htmlWebpackPlugins
    }
}

静态资源内联

资源内联(inline resource),是将一个资源以内联的方式嵌入另一个资源里面。

主要目的是监控上报相关打点、减少维护成本、提高页面加载性能及交互体验。

  1. HTML 内联使用raw-loader@0.5.1,多应用在多页共用相同的 meta 头,安装后在模板index.html引入即可。
<!DOCTYPE html>
<html lang="en">
  <head>
    <%= require('raw-loader!./meta.html') %>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
  1. CSS 内联在基础篇也有讲过用mini-css-extract-plugin将产生的所有 CSS 提取成一个独立的文件,以 link 方式引入。若想以 style 标签包裹嵌入 head 标签需要添加html-inline-css-webpack-plugin来实现 CSS 内联的功能,。
+ const HTMLInlineCSSWebpackPlugin = require('html-inline-css-webpack-plugin').default

plugins: [
  new VueLoaderPlugin(),
  new MiniCssExtractPlugin({
    filename: '[name][contenthash:8].css'
  }),
  new HtmlWebpackPlugin({
    template: './src/index.html',
    inject: true,
    filename: 'index.html'
  }),
+  new HTMLInlineCSSWebpackPlugin()
]
  1. JS 内联也可使用raw-loader@0.5.1,如引用lib-flexible, index.html 添加行。
<!DOCTYPE html>
<html lang="en">
  <head>
    <%= require('raw-loader!./meta.html') %>
+    <script>
+     <%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %>
+    </script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
  1. 图片、字体内联借助url-loader,当图片或字体文件大小小于 10k 可转base64。webpack5已内置,可使用asset/inline替代。

基础库分离

使用 webpack 提供的 externals 的配置,防止把某些 import 的包打包到最终的bundle中,而是在运行中再去外部获取这些扩展依赖,优化压缩打包后文件大小以提高页面响应速度。

以 Vue 项目为例,可以将诸如 vue、vue-router、vuex 等以 cdn 的方式引入。以 vue 为例,在 webpack.prod.js 增加如下代码。

module.exports = {
+  externals: {
+    vue: 'Vue'
+  }
}

使用官方推荐的 unpkgjsDelivr 的 CDN 网站复制链接,在 index.html 添加如下代码。

<!DOCTYPE html>
<html lang="en">
  <head>
    <%= require('raw-loader!./meta.html') %>
    <script defer="defer"> 
     <%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %>
    </script>
+   <script defer="defer" src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

打包后可发现 main.js 如今只有7kb

image.png

除这种方式之外,webpack4 后内置的SplitChunksPlugin插件也可将公共代码分离出来。

看下SplitChunk的默认配置

optimization: {
  splitChunks: {
    // 哪些 chunks 进行分割,可选值:async、initial、all
    chunks: 'async',
    // 分离的 chunk 必须大于等于 minSize,默认20000, 约20kb
    minSize: 20000,
    // 通过拆分后剩余的最小 chunk 体积不能小于 0 。development 模式中默认为 0,其他情况,默认为 minSize 的值
    minRemainingSize: 0,
    // 分离的 chunk 至少被引用 1次
    minChunks: 1,
    // 按需加载文件,并行请求的最大数目
    maxAsyncRequests: 30,
    // 加载入口文件,并行请求的最大数目
    maxInitialRequests: 30,
    // 执行拆分的大小阈值,其他限制(minRemainingSize、maxAsyncRequests、maxInitialRequests)将被忽略
    enforceSizeThreshold: 50000,
    // cacheGroups 可配置多个组,每个组根据 test 设置条件,符合 test 条件的模块,就分配到该组。模块可被多个组引用,最终根据 priority 决定打包到哪个组。默认将所有 node_modules 目录打包至 vendors 组,将两个以上的 chunk 所共享的模块打包至 default 组
    cacheGroups: {
      defaultVendors: {
        test: /[\\/]node_modules[\\/]/, 
        // 缓存组打包的先后优先级
        priority: -10, 
        // 设置是否重用当前 chunk
        reuseExistingChunk: true 
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true
      }
    }
  }
}

若项目本身,更改 chunks 为 'all',即可将我们的基础库 vue 等进行一个分离,且可单独以 name 字段命名每一个 chunk。

Eslint

Eslint 目的是统一的团队代码格式,提高代码的可读性、可维护性。

步骤如下,最终会生成一份 .eslintrc.js 文件。

# 安装 eslint
npm i eslint -D
# 初始化配置文件,按提示操作即可
npx eslint --init

Eslint 官网指南 🐱‍🏍

优化命令行的构建日志

通过 stats 字段更精确地控制 bundle 信息怎么显示,常见有如下几种选项。

预设描述
errors-only只在发生错误时输出
errors-warnings只在发生错误或新的编译时输出
minimal只在发生错误或新的编译开始时输出
none没有输出
normal标准输出
verbose全部输出

可在开发和生产环境中将 stats 字段配置成 errors-only,仅显示错误信息。

在设置 stats 字段的基础上使用 friendly-errors-webpack-plugin 美化输出信息。

安装 npm i friendly-errors-webpack-plugin -D,plugins 增加配置。

plugins: [
  new VueLoaderPlugin(),
  new MiniCssExtractPlugin({
    filename: '[name][contenthash:8].css'
  }),
  new HtmlWebpackPlugin({
    template: './src/index.html',
    inject: true,
    filename: 'index.html'
  }),
  new HTMLInlineCSSWebpackPlugin(),
+  new FriendlyErrorsWebpackPlugin()
]

配置完成,可打包进行测试。

抽离公共配置

目前,开发和生产环境存在着重复的代码,所以,需要抽离公共配置。为了将这些配置合并,需要 webpack-merge 工具。

安装 npm i webpack-merge -D,并新建 webpack.common.js 文件。

直接放抽离结果 ~

const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HTMLInlineCSSWebpackPlugin = require('html-inline-css-webpack-plugin').default
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const fileRoot = process.cwd()

module.exports = {
  entry: './src/main.js',
  output: {
    path: path.join(fileRoot, 'dist'),
    filename: '[name]_[chunkhash:8].js',
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  'autoprefixer',
                  [
                    'postcss-px-to-viewport',
                    {
                      viewportWidth: 1920,
                      unitPrecision: 5,
                      minPixelValue: 1
                    }
                  ]
                ]
              }
            }
          }
        ]
      },
      {
        test: /.(png|jpg|gif|jpeg)$/,
        type: 'asset/resource',
        generator: {
          filename: '[name][hash:8].[ext]'
        }
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        type: 'asset/resource',
        generator: {
          filename: '[name][hash:8].[ext]'
        }
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name][contenthash:8].css'
    }),
    new HTMLInlineCSSWebpackPlugin(),
    new FriendlyErrorsWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      inject: true,
      filename: 'index.html'
    })
  ],
  stats: 'errors-only'
}

webpack.dev.js 简化如下。

const webpackMerge = require('webpack-merge')
const commonConfiguration = require('./webpack.common.js')

const developmentConfiguration = {
  mode: 'development',
  devServer: {
    port: 3000,
    hot: true,
    open: true
  }
}

module.exports = webpackMerge.merge(commonConfiguration, developmentConfiguration)

webpack.prod.js 简化如下,基础库的分离二选一。

const webpackMerge = require('webpack-merge')
const commonConfiguration = require('./webpack.common.js')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')

const productionConfiguration = {
  mode: 'production',
  plugins: [],
  optimization: {
    minimizer: [new TerserPlugin(), new CssMinimizerPlugin()]
    // splitChunks: {
    //   chunks: 'all',
    //   cacheGroups: {
    //     vendors: {
    //       test: /[\\/]node_modules[\\/]/,
    //       // 缓存组打包的先后优先级
    //       priority: -10,
    //       // 设置是否重用当前 chunk
    //       reuseExistingChunk: true,
    //       name: 'vendor'
    //     },
    //     default: {
    //       minChunks: 2,
    //       priority: -20,
    //       reuseExistingChunk: true
    //     }
    //   }
    // }
  },
  externals: {
    vue: 'Vue'
  }
}
module.exports = webpackMerge.merge(commonConfiguration, productionConfiguration)

速度分析

安装 npm i speed-measure-webpack-plugin -D

在 webpack.prod.js 文件的 plugins 的配置中加入插件。

const webpackMerge = require('webpack-merge')
const commonConfiguration = require('./webpack.common.js')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
+ const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin')

const productionConfiguration = {
  mode: 'production',
  plugins: [
+   new SpeedMeasureWebpackPlugin()
  ],
  optimization: {
    minimizer: [new CssMinimizerPlugin()]
    // splitChunks: {
    //   chunks: 'all',
    //   cacheGroups: {
    //     vendors: {
    //       test: /[\\/]node_modules[\\/]/,
    //       // 缓存组打包的先后优先级
    //       priority: -10,
    //       // 设置是否重用当前 chunk
    //       reuseExistingChunk: true,
    //       name: 'vendor'
    //     },
    //     default: {
    //       minChunks: 2,
    //       priority: -20,
    //       reuseExistingChunk: true
    //     }
    //   }
    // }
  },
  externals: {
    vue: 'Vue'
  }
}
module.exports = webpackMerge.merge(commonConfiguration, productionConfiguration)

打包完成后,可输出各个 loader、plugin 的解析时间,如下所示。

image.png

体积分析

安装 npm i webpack-bundle-analyzer -D,帮助分析项目依赖第三方模块和业务代码的大小。

和速度分析插件配置一致,在 webpack.prod.js 的 plugins 字段引入即可。

+ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

plugins: [
  new SpeedMeasureWebpackPlugin(), 
+  new BundleAnalyzerPlugin()
]

根据生成的矩阵树图做针对性优化。

多线程并行构建

当项目变的臃肿后,需要采取多进程打包去优化构建。若项目体积不大,则无需开启多进程打包,因为开启进程同样需要花费时间。

安装 npm i thread loader -D。至于 happypack 插件目前已不再维护,作者本身也是推荐使用thread loader

在 webpack.common.js 中针对耗时较长的文件添加 thread-loader,如解析 js 。

注意: 放置在其他 loader 之前,放置在 thread-loader 后的 loader 会在一个单独的 worker 池中。

rules: [
  {
    test: /\.js$/,
    use: [
+      {
+        loader: 'thread-loader',
+        options: {
+          worker: 2
+        }
+      },
      {
        loader: 'babel-loader'
      }
    ]
  }
]

多线程并行压缩

在基础篇有谈到关于 JS 压缩使用 TerserPlugin。

并行需要将 parallel 字段设置为 true,同样只适用于项目 JS 文件过大的情况。

optimization: { 
  minimizer: [
    new TerserPlugin({
+      parallel: true
    }),
    new CssMinimizerPlugin()
  ]
}

DllPlugin

DllPlugin 作用是预编译资源模块,意思是提前将依赖库进行编译打包,生成一个动态链接库。与 externals 相比不会生成多个 script 标签,与 splitChunks 相比减少项目打包时的编译解析时间。

第一步,新增 webpack.dll.js 文件。

const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: {
    library: ['vue']
  },
  mode: 'production',
  output: {
    filename: '[name].dll.[hash].js',
    path: path.join(__dirname, 'dist/dll'),
    // library 需和 Dllplugin 中的 name 一致
    library: '[name]_dll'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_dll',
      path: path.join(__dirname, 'dist/dll/[name].json')
    })
  ]
}

第二步,package.json 文件新增命令 npm run dll 命令。

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack --config webpack.prod.js",
  "dev": "webpack-dev-server --config webpack.dev.js",
+  "dll": "webpack --config webpack.dll.js"
},

执行命令后,生成 dist/dll 目录,其中的 json 文件用于让 DllReferencePlugin 映射到相关的依赖上。

为了后续打包构建不删除 dll 目录,将 webpack.common.js 中的 clean 修正一下,删除忽略 dll 目录。

clean: {
  keep: /dll/
}

第三步,在 webpack.prod.js 文件中的 plugins 字段新增 DllReferencePlugin 配置。

plugins: [
  new SpeedMeasureWebpackPlugin(),
  new BundleAnalyzerPlugin(),
+  new webpack.DllReferencePlugin({
+    manifest: require('./dist/dll/library.json')
+  })
]

最后一步,在 index.html 上新增 script 标签。

<script defer="defer" src="dll/library.dll.c4b5e421aaf7688e87c9.js"></script>

完成后打包进行测试。

缓存

首先,可将解析 JS 文件的 babel-loader 配置 cacheDirectory 为 true。其次,可配置 TerserPlugin 配置 cache 字段为true(最新版本已兼容缓存功能,4之前可配置)。

最后开启模块缓存,可使用 cache-loader 或者 HardSourceWebpackPlugin 。

cache-loader 使用方式即在开销较大的 loader 前添加 cache-loader,将结果缓存到磁盘中,如放置在 css-loader 之前。

HardSourceWebpackPlugin 无法在 v5 版本下实施,原因与解决方案

缩小构建目标

首先,比如解析 js、css 等文件时使用 exclude/include 字段确保转义尽可能少的文件,如 include 配置解析 src 目录下的文件或者 exclude 配置 node_modules 目录。

其次,减少文件搜索范围,主要字段是 resolve。

首先是 modules 字段,其默认值是['node_modules'],目的在于告知 webpack 解析模块时应该搜索的目录,可添加如 path.resolve(__dirname, 'src'),便于引入公共组件文件import 'components/button'或者公共类import 'utils'

第二是 alias 字段,将原来的模块路径映射成新的导入路径,与resolve.modules不同,作用在于碰到别名,直接去对应目录下查找模块,减少搜索时间。常见的配置是alias: { '@': path.resolve(__dirname, 'src') }

第三是 mainFields 字段,mainFields 告知 webpack 使用哪个字段来导入模块。web 下默认值为["browser", "module", "main"],顺序从左到右,若第三方模块大都采用 main 字段,所以可以将 main 字段放置在最前面,最终根据实际情况来定。

最后是 extensions 字段,目的是自动带入后缀去匹配对应文件。默认值是['js', 'json'],导入时尽量把后缀名带上,避免查找。

总结

拖得最久的一篇文章,也是 2022 年的第一篇文章。

还有原理篇,喜欢的朋友可以 点赞 + 收藏 + 关注 ~