记一次webpack5.x升级过程

背景

历史原因前一次升级是将webpack1.x版升级到3.x, 主要使用happyPack+dllPlugins; 具体配置可前往:www.yuque.com/donsoft/qkn...;

项目模块数庞大,重构可能性不大 ,在不影响业务开发及devops正常流程,索性在3.x的基础上升级到5

官方资料

官网:webpack.docschina.org/

源代码仓库:github.com/webpack/web...

源码压缩包下载:📎webpack-main.zip

构建工作流程:

  1. 初始化参数:取配置文件和shell脚本参数并合并
  2. 开始编译:用上一步得到的参数初始化compiler对象,执行run方法开始编译
  3. 确定入口:根据配置中的entry,确定入口文件
  4. 编译模块:从入口文件出发,递归遍历找出所有依赖模块的文件
  5. 完成模块编译:使用loader转译所有模块,得到转译后的最终内容和依赖关系
  6. 输出资源:根据入口和模块依赖关系,组装成一个个chunk,加到输出列表
  7. 输出完成:根据配置中的output,确定输出路径和文件名,把文件内容写入输出目录(默认是dist)

前置说明:务必注意 -g/--save/--save-dev 的区分

全局安装:npm install webpack -g

可以通过npm config list命令 查看全局安装的包

局部安装:npm install webapck --save-dev 或 npm install webpack -D

npm install -g moduleName 命令

  1. 安装模块到全局,不会在项目node_modules目录中保存模块包。

  2. 不会将模块依赖写入devDependencies或dependencies 节点。

  3. 运行 npm install 初始化项目时不会下载模块。

npm install -save moduleName 命令

  1. 安装模块到项目node_modules目录下

  2. 会将模块依赖写入dependencies 节点。

  3. 运行 npm install 初始化项目时,会将模块下载到项目目录下。

  4. 运行npm install --production或者注明NODE_ENV变量值为production时,自动下载模块到node_modules目录中。

npm install -save-dev moduleName 命令

  1. 安装模块到项目node_modules目录下。

  2. 会将模块依赖写入devDependencies 节点。

  3. 运行 npm install 初始化项目时,会将模块下载到项目目录下。

特别说明:devDependencies与dependencies的区别

开发环境(devDependencies)

devDependencies下列出的模块,是开发时用的依赖项,像一些模块打包器,比如webpack,我们用它打包js文件,只用于开发环境,不会被部署到生产环境。

生产环境(dependencies):

dependencies下列出的模块,则是生产环境中需要的依赖,即正常运行该包时所需要的依赖项,是需要部署到生产环境的。如:react、antd等

在安装包依赖时请务必注意区分。


一、开始工作

优化开发体验、加快编译速度、减小打包体积、加快加载速度,代码分割,持久化缓存,模块联邦等 维度,介绍如何对 webpack 项目进行配置最优。

依赖的 webpack 版本信息如下:

diff 复制代码
-   node v16.13.1
-   npm v7.x.x
-   webpack-cli\@4.7.2
-   webpack\@5.69.1

二、优化工具

安装以下 webpack 插件,帮助我们分析优化效率:

scss 复制代码
-   progress-bar-webpack-plugin |  webpackbar:// 查看编译进度;
-   speed-measure-webpack-plugin: // 查看编译速度;
-   webpack-bundle-analyzer:// 打包体积分析

1. 进度条工具

通过 progress-bar-webpack-plugin 插件查看编译进度,方便在npm run dev 时查看编译情况。

安装:

css 复制代码
npm i progress-bar-webpack-plugin -D
npm i webpackbar -D

webpack.common.js 配置方式如下:

javascript 复制代码
//进度百分比添加了加粗和绿色高亮态样式
const chalk = require('chalk')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
module.exports = {
  plugins: [
    // 进度条
    new ProgressBarPlugin({
        format: `  :msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`
      })
  ],
}

//简易配置
const WebpackBar = require('webpackbar')
const ProgressBarWebpackPlugin = require('progress-bar-webpack-plugin')
module.exports = {
    // ...
    plugins: [
      new WebpackBar(),
      new ProgressBarWebpackPlugin(),
    ],

包含内容、进度条、进度百分比、消耗时间,进度条效果如下:

本次配置中使用WebpackBar的配置


2. 编译速度分析

优化 webpack 构建速度,首先需要知道是哪些插件、哪些 loader 耗时长,方便有针对性的优化。

通过 speed-measure-webpack-plugin 插件进行构建速度分析,可以看到各个 loader、plugin 的构建时长,可针对耗时 loader、plugin 进行优化。

安装:

css 复制代码
npm i  speed-measure-webpack-plugin -D

webpack.dev.js 配置方式如下:

ini 复制代码
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
  // ...
})

// 简易配置(此次配置中使用此方案)
plugiins:[
  new SpeedMeasurePlugin();
]

包含各工具的构建耗时,效果如下:

三、提升开发体验

1. 自动热新

自动更新 指的是,在开发过程中,修改代码后,无需手动再次编译,可以自动编译代码更新编译后代码的功能。

webpack 提供了以下几种可选方式,实现自动更新功能:

vbscript 复制代码
webpack's Watch Mode
webpack-dev-server 4.x; webpack-server 5.x**
webpack-dev-middleware

webpack 官方推荐的方式是 webpack-dev-server[5.x版本以下,建议使用react-hot-loader]

5.X版本中使用@pmmmwh/react-refresh-webpack-plugin 侵入性更少 ,配置简单

所需要版本信息:

Dependency Version
react 16.13.0+ or 17.x
react-dom 16.13.0+ or 17.x
react-refresh 0.10.0+
webpack 4.46.0+ or 5.2.0+

这是针对开发环境的优化,修改 webpack.dev.js 配置。

2. 热更新

热更新 指的是,在开发过程中,修改代码后,仅更新修改部分的内容,无需刷新整个页面。

2.1 修改 webpack-dev-server 配置

关于devserver的详解

使用 webpack 内置的 HMR 插件,更新 webpack-dev-server 配置。

webpack.dev.js 配置方式如下:

arduino 复制代码
module.export = {
    devServer: {
        contentBase: path.resolve(__dirname, "dist"),
        port: "4099",
        // proxy: process.env.ENV === 'loc' ? { //反向代理,根据需求自行修改
        //     "/api": {
        //         target: "http://127.0.0.1:3001",
        //         pathRewrite: {
        //             "^/api": ""
        //           }
        //     }
        // }:{},
        open: true,
        host: 'localhost',
        hotOnly: false, // 页面构建失败不刷新页面
        hot: true, //让webpackDevServer开启热更新功能
    },
}

2.2 引入 react-refresh-webpack-plugin

使用 react-refresh-webpack-plugin 热更新 react 组件。

安装:

bash 复制代码
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

webpack.dev.js 配置方式如下:

ini 复制代码
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new ReactRefreshWebpackPlugin(),
    ]
}

遇到的问题:

配置了 SpeedMeasurePlugin 插件后后,热更新无效了,会提示 runtime is undefined。


解决方案:

仅在需要分析构建速度时打开 SpeedMeasurePlugin 插件,否则注释掉;此次配置将注释它

最终效果:

更新 react 组件代码时,无需刷新页面,仅更新组件部分。


四、提升构建速度

1. 更新版本

1.1 webpack 版本

使用最新的 webpack 版本,通过 webpack 自身的迭代优化,来加快构建速度。比如增加的持久化缓存

webpack5 新特性可查看

webpack.js.org/plugins/ign...

www.jianshu.com/p/dfd794119...

1.2 包管理工具版本

Node.js 、package 管理工具(例如 npm / yarn / pnpm)更新到最新版本,也有助于提高性能。较新的版本能够建立更高效的模块树以及提高解析速度。本次演示依赖的版本信息如下:

同时约束只能使用npm的原因也是借助package-lock.json 强大缓存的原因

2. 缓存

2.1 cache

通过配置 webpack 持久化缓存 cache: filesystem,来缓存生成的 webpack 模块和 chunk,改善构建速度。

简单来说,通过 cache: filesystem 可以将构建过程的 webpack 模板进行缓存,大幅提升二次构建速度、打包速度,

当构建突然中断,二次进行构建时,可以直接从缓存中拉取,可提速 90% 左右。

2.2 dll 已废弃

webpack 官网构建性能 关于 dll 的介绍:

dll 可以为更改不频繁的代码生成单独的编译结果。可以提高应用程序的编译速度。

webpack5 开箱即用的持久缓存是比 dll 更优的解决方案。

dll+happypack方案详见www.yuque.com/donsoft/qkn...

2.3 cache-loader 已废弃

3. 减少 loader、plugins

每个的 loader、plugin 都有其启动时间。尽量少地使用工具,将非必须的 loader、plugins 删除。

3.1 指定 include

为 loader 指定 include,减少 loader 的查找范围,。

webpack 构建性能文档

rule.exclude 可以排除模块范围,也可用于减少 loader 应用范围

webpack.common.js 配置方式见壳工程:

定义 loader 的 include 后,构建时间将减少 12%,效果如下:

3.2 管理资源

使用 webpack 资源模块 (asset module) 代替旧的 assets loader(如 file-loader/url-loader/raw-loader/json-loader/ 等),减少 loader 配置数量。

webpack.common.js 配置方式如下:

bash 复制代码
{
        test: /.(png|gif|bmp|jpg)$/,
        type: 'asset/resource',
      },
      {
        test: /.(woff(2)?|eot|ttf|otf|svg|)$/,
        type: 'asset/inline',
      },

4. 优化 resolve 配置

resolve 用来配置 webpack 如何解析模块,可通过优化 resolve 配置来覆盖默认配置项,减少解析范围。

4.1 alias

alias 可以创建 import 或 require 的别名,用来简化模块引入。

less 复制代码
  alias: {
      pages: path.join(__dirname, 'src/pages'),
      widget: path.join(__dirname, 'src/widget'),
      router: path.join(__dirname, 'src/router'),
      utils: path.join(__dirname, 'src/utils'),
      assets: path.join(__dirname, 'src/assets'),
      themes: path.join(__dirname, 'src/themes'),
      config: path.join(__dirname, 'src/config'),
      'yx-widget': path.join(__dirname, 'src/yx-widget'),
    },

4.2 extensions

extensions 表示需要解析的文件类型列表。

根据项目中的文件类型,定义 extensions,以覆盖 webpack 默认的 extensions,加快解析速度。

由于 webpack 的解析顺序是从左到右,因此要将使用频率高的文件类型放在左侧,如下我将 jsx 放在最左侧。

perl 复制代码
  extensions: ['.jsx', '.web.js', '.js', '.json', '.css'], //去掉['',]空

4.3 modules

modules 表示 webpack 解析模块时需要解析的目录。

指定目录可缩小 webpack 解析范围,加快构建速度。

csharp 复制代码
 modules: [path.join(__dirname, 'src'), 'node_modules'], // 仅在项目目录下查找node_modules,找不到则不会往上层找(全局)

如果项目不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false,减少解析工作量。

4.5 mainFiles

和npm link 类似,所有模块统一从package.json文件中找入口文件, 找不到则直接找项目根目录入口文件

vbnet 复制代码
mainFields: ['loader', 'main'], 
mainFiles: ['index.js'],

webpack.common.js 配置方式:

css 复制代码
resolve: {
    alias: {
      pages: path.join(__dirname, 'src/pages'),
      widget: path.join(__dirname, 'src/widget'),
      router: path.join(__dirname, 'src/router'),
      utils: path.join(__dirname, 'src/utils'),
      assets: path.join(__dirname, 'src/assets'),
      themes: path.join(__dirname, 'src/themes'),
      config: path.join(__dirname, 'src/config'),
      "yx-widget": path.join(__dirname, 'src/yx-widget'),
    },
    modules: [path.join(__dirname, 'src'), 'node_modules'],
    extensions: ["", ".js", ".jsx", '.less', '.css'],//配置文件后缀缩小查找范围 
    mainFields: ['loader', 'main'], // 从package.json文件中找入口文件。
    mainFiles: ['index.js'],
    symlinks: false,
  },
  resolveLoader: {
    // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    // 其中 __dirname 表示当前工作目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')],

  },
  // cdn配置后,index.html  引入cdn链接
  externals: {
    // 'react': 'React',
    // 'react-dom': 'ReactDOM',
    // 'react-router-dom': 'ReactRouterDOM'
  },

优化 resolve 配置后,构建时间将减少 1.5%

5. 多进程

测试效果中 babel-loader 的构建时间占据了整个构建过程的 50%,通过多进程将Loader放在一个独立的 worker 池中运行,就不会阻碍其他 loader 的构建,可以加快构建速度。

5.1 thread-loader

通过 thread-loader ****将耗时的 loader 放在一个独立的 worker 池中运行,加快 loader 构建速度。

安装:npm i -D thread-loader

官方配置:
webpack.js.org/loaders/thr...

注意:如果使用的是sass

webpack 官网 提到 node-sass 中有个来自 Node.js 线程池的阻塞线程的 bug。当使用 thread-loader 时,需要设置 workerParallelJobs: 2。

thread-loader会对其后面的loader(这里是babel-loader)开启多进程打包。

进程启动大概为600ms,进程通信也有开销。(启动有一定开销,不要滥用)

只有工作消耗时间比较长,才需要多进程打包

thread-loader必须最后执行,再次说明loader是从下往上,从右往左的执行顺序,所以想要使用thread-loader优化某项的打包速度,必须放在其后执行

由于 thread-loader 引入后,需要 0.6s 左右的时间开启新的 node 进程,如果工程代码量小,引入 thread-loader 后,构建时间反而增加了0.19s,适得其反

因此,建议大型(超过150个页面)项目的优化时,仅在非常耗时的 loader 前引入 thread-loader即可。

5.2 happypack 已废弃

happypack 同样是用来设置多线程,但是在 webpack5官方也已经不再维护了,推荐使用 thread-loader。

6. 区分环境

Webpack5 模式(mode) 的不同模式的内置优化。

在开发过程中,切忌在开发环境使用生产环境才会用到的工具,如在开发环境下,应该排除 [fullhash]/[chunkhash]/[contenthash] 等工具。

同样,在生产环境,也应该避免使用开发环境才会用到的工具,如 webpack-dev-server 等插件。

7. 其他devtool

7.1 devtool的不同配置

不同的 devtool 设置,会导致性能差异。

在大多数情况下,最佳选择是 eval-cheap-module-source-map

详细区分可到官网 webpack devtool查看。其它参考站点

webpack.dev.js 配置方式如下:

arduino 复制代码
export.module = {
    devtool: 'eval-cheap-module-source-map',
}

7.2 输出结果不携带路径信息

默认 webpack 会在输出的 bundle 中生成路径信息,将路径信息删除可小幅提升构建速度。

ini 复制代码
module.exports = {
    output: {
        pathinfo: false,
      },
    };
}

五、减小打包体积

1. 代码压缩

体积优化第一步是压缩代码,通过 webpack 插件,将 JS、CSS 等文件进行压缩。

1.1 JS 压缩

使用 TerserWebpackPlugin 来压缩 JavaScript。

webpack v5 开箱即带有最新版本的 terser-webpack-plugin。

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

如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

terser-webpack-plugin 默认开启了 parallel: true 配置,并发运行的默认数量:os.cpus().length - 1

,本文配置的 parallel 数量为 4,使用多进程并发运行压缩以提高构建速度。

官方配置

| 选项名 | 类型 | 默认值 | 描述 |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------|------------------|--------------|------|--------------------|
| test | String | RegExp | Array<String | RegExp> | /.m?js(?.*)?$/i | 用来匹配需要压缩的文件。 |
| include | String | RegExp | Array<String | RegExp> | undefined | 匹配参与压缩的文件。 |
| exclude | String | RegExp | Array<String | RegExp> | undefined | 匹配不需要压缩的文件。 |
| parallel | Boolean | Number | true | 使用多进程并发运行以提高构建速度。 |
| minify | Function | TerserPlugin.terserMinify | 允许你自定义压缩函数。 |
| terserOptions | Object | default | Terser 的 minify 选项。 |
| extractComments | Boolean | String | RegExp | Function<(node, comment) -> Boolean | Object> | Object | true | 注释是否需要提取到一个单独的文件中。 |

1.1.2 ParallelUglifyPlugin 已废弃

ParallelUglifyPlugin 插件也可以帮助我们多进程压缩 JS,webpack5 的 TerserWebpackPlugin 默认就开启了多进程和缓存,无需再引入 ParallelUglifyPlugin。

1.2 CSS 压缩

使用 CssMinimizerWebpackPlugin 压缩 CSS 文件。和 optimize-css-assets-webpack-plugin 相比,

css-minimizer-webpack-plugin 在 source maps 和 assets 中使用查询字符串会更加准确,而且支持缓存和并发模式下运行。

CssMinimizerWebpackPlugin 将在 Webpack 构建期间搜索 CSS 文件,优化、压缩 CSS。

安装:

npm install -D css-minimizer-webpack-plugin

2. 代码分离

代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,可以缩短页面加载时间。

2.1 抽离重复代码

SplitChunksPlugin--原理刨析; 插件开箱即用,可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。

webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹;
  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积);
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30时,开发环境才生效;
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30;

通过 splitChunks 把 react 等公共库抽离出来,不重复引入占用体积。

注意:不要为 cacheGroups 定义固定的 name,因为 cacheGroups.name 指定字符串或始终返回相同字符串的函数时,会将所有常见模块和 vendor 合并为一个 chunk。这会导致更大的初始下载量并减慢页面加载速度。

SplitChunksPlugin参数说明:

chunks: 指的的那些chunks需要进行优化,是一个字符串类型,有效值是:all,async和initial。

diff 复制代码
-   async这个值表示按需引入的模块将会被用于优化。

-   initial表示项目中被直接引入的模块将会被用于优化。

-   all顾名思义,表明直接引入和按需引入的模块都会被用于优化。
  • minSize: 打包优化完生成的新chunk大小要> 30000字节,否则不生成新chunk。
  • minChunks: 共享该module的最小chunk数
  • maxAsyncRequests:最多有N个异步加载请求该module
  • maxInitialRequests: 一个入口文件可以并行加载的最大文件数量
  • automaticNameDelimiter:名字中间的间隔符
  • name:chunk的名字,

如果设成true,会根据被提取的chunk自动生成。
值为 false 时,适合生产模式使用,webpack 会避免对 chunk 进行不必要的命名,以减小打包体积,

  • 除了入口 chunk 外,其他 chunk 的名称都由 id 决定,

所以最终看到的打包结果是一排数字命名的 js,这也是为啥我们看线上网页请求的资源,总会掺杂一些 0.js,1.js 之类的文件(当然,使资源名为数字 id 的方式不止这一种,懒加载也能轻松办到)。

值为 string 时,缓存组最终会打包成一个 chunk,名称就是该 string。此外,当两个缓存组 name 一样,最终会打包在一个 chunk 中。你甚至可以把它设为一个入口的名称,从而将这个入口会移除。

cacheGroups: 这个就是重点了,我们要切割成的每一个新chunk就是一个cache group。

test:用来决定提取哪些module,可以接受字符串,正则表达式,或者函数,函数的一个参数为module,第二个参数为引用这个module的chunk(数组)。

priority:优先级高的chunk为被优先选择(说出来感觉好蠢),优先级一样的话,size大的优先被选择。

reuseExistingChunk: 当module未变时,是否可以使用之前的chunk。

要禁用任何默认缓存组,请将它们设置为false。例如 default:false

配置详解:

less 复制代码
splitChunks: {
      chunks: 'async', // 代码分割时对异步代码生效,all:所有代码有效,inital:同步代码有效
      minSize: 30000, // 代码分割最小的模块大小,引入的模块大于 30000B 才做代码分割
      maxSize: 0, // 代码分割最大的模块大小,大于这个值要进行代码分割,一般使用默认值
      minChunks: 1, // 引入的次数大于等于1时才进行代码分割
      maxAsyncRequests: 6, // 最大的异步请求数量,也就是同时加载的模块最大模块数量
      maxInitialRequests: 4, // 入口文件做代码分割最多分成 4 个 js 文件
      automaticNameDelimiter: '~', // 文件生成时的连接符
      automaticNameMaxLength: 30, // 自动生成的文件名的最大长度
      cacheGroups: {
        vendors: {
          test: /[\/]node_modules[\/]/, // 位于node_modules中的模块做代码分割
          priority: -10 // 根据优先级决定打包到哪个组里,例如一个 node_modules 中的模块进行代码
        }, // 分割,,既满足 vendors,又满足 default,那么根据优先级会打包到 vendors 组中。
        default: { // 没有 test 表明所有的模块都能进入 default 组,但是注意它的优先级较低。
          priority: -20, //  根据优先级决定打包到哪个组里,打包到优先级高的组里。
          reuseExistingChunk: true // //如果一个模块已经被打包过了,那么再打包时就忽略这个上模块
        }
      }
    }

壳工程中配置:

less 复制代码
splitChunks: {
      chunks: 'all',
      cacheGroups:{
        vendors:{ // node_modules里的代码
          test: /[\/]node_modules[\/]/,
          chunks: "all",
          name: 'false', //不要定义固定的name
          priority: 10, // 优先级
          enforce: true
        }
      }
    },

2.2 CSS 文件分离

MiniCssExtractPlugin 插件将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。

安装:npm install -D mini-css-extract-plugin

注意: MiniCssExtractPlugin.loader 要放在 style-loader 后面。 //不兼容webpack5,改用css-minimizer-webpack-plugin插件

2.3 最小化 entry chunk

通过配置 optimization.runtimeChunk = true,为运行时代码创建一个额外的 chunk,减少 entry chunk 体积,提高性能。

webpack.prod.js 配置方式如下:

ini 复制代码
module.exports = {
    optimization: {
        runtimeChunk: true,
      },
    };
}

3. Tree Shaking(摇树)

摇树,通俗讲代表项目中未引用的无用代码

3.1 JS

JS Tree Shaking 将 JavaScript 上下文中的未引用代码(Dead Code)移除,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。

Dead Code 一般具有以下几个特征:

  • 代码不会被执行,不可到达;
  • 代码执行的结果不会被用到;
  • 代码只会影响死变量(只写不读)。

3.1.1 webpack5 sideEffects

通过 package.json 的 "sideEffects" 属性,来实现这种方式。

json 复制代码
{
  "name": "your-project",
  "sideEffects": false
}

注意的是,当代码有副作用时,需要将 sideEffects 改为提供一个数组,添加有副作用代码的文件路径:

json 复制代码
{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js"]

实际项目中配置为"sideEffects": false,未知副作用的包及js 始料未及

3.2 CSS

CSS 代码也需要摇摇树,打包时把没有用的 CSS 代码去除,可以大幅减少打包后的 CSS 文件大小。

使用 purgecss-webpack-plugin[50] 对 CSS Tree Shaking。

安装:npm i purgecss-webpack-plugin -D

因为打包时 CSS 默认放在 JS 文件内,因此要结合 webpack 分离 CSS 文件插件 mini-css-extract-plugin 一起使用,先将 CSS 文件分离,再进行 CSS Tree Shaking。

3.3 CDN

webpack 配置的优化,另一方面还可以通过 CDN 来减小打包体积。

这里引入 CDN 的首要目的为了减少打包体积,因此仅仅将一部分大的静态资源手动上传至 CDN,并修改本地引入路径。下文的加快加载速度,将介绍另一种 CDN 优化手段。

将大的静态资源上传至 CDN:

  • 字体:压缩并上传至 CDN;
  • 图片:压缩并上传至 CDN。

3.4 打包内容分析

webpack.prod.config.js配置:

javascript 复制代码
const BundleAnalyzerPlugin  = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;//打包内容分析
new BundleAnalyzerPlugin({
      analyzerMode: "server",
      analyzerHost: "127.0.0.1",
      analyzerPort: 8888, // 运行后的端口号
      reportFilename: "report.html",
      defaultSizes: "parsed",
      openAnalyzer: true,
      generateStatsFile: false,
      statsFilename: "stats.json",
      statsOptions: null,
      logLevel: "info"
    }),

效果分析:

根据报告结果分析,大文件可以再分包处理

六、加快加载速度

1. 按需加载

通过 webpack 提供的 import() 语法 ,动态导入 功能进行代码分离,通过按需加载,大大提升网页加载速度。

2. 浏览器缓存

加载的静态资源被浏览器缓存,再次进入网站后,将直接拉取缓存资源,加快加载速度。

webpack 支持根据资源内容,创建 hash id,当资源内容发生变化时,将会创建新的 hash id。

配置 JS bundle hash,webpack.common.js 配置方式如下:

java 复制代码
module.exports = {
  // 输出
  output: {
    // 仅在生产环境添加 hash
   filename: isEnvProduction ? '[name]-v-[fullhash].js' : '[name].js', // 定义输出文件名
  },
}

配置 CSS bundle hash,webpack.prod.js 配置方式如下:

java 复制代码
module.exports = {
  plugins: [
    // 提取 CSS
    new MiniCssExtractPlugin({
       filename: "css/[name].[contenthash].css"
    }),
  ],
}

配置 optimization.moduleIds,让公共包 splitChunks 的 hash 不因为新的依赖而改变,减少非必要的 hash 变动,webpack.prod.js 配置方式如下:

css 复制代码
module.exports = {
  optimization: {
    moduleIds: 'deterministic',
  }
}

通过配置 contenthash,浏览器缓存了未改动的文件,仅重新加载有改动的文件,加快加载速度。

3. CDN

将所有的静态资源,上传至 CDN,通过 CDN 加速来提升加载速度。

webpack.common.js 配置方式如下:

javascript 复制代码
// 项目中尽可能使用客户提供的资源
export.modules = {
output: {
    publicPath: isEnvProduction ? 'https://xxx.com' : '', // CDN 域名
  },
}

七、优化前后对比

查看优化前后对比。

1. 构建速度run dev时

类型 首次运行 未修改内容二次运行 修改内容二次运行
优化前 3342ms 1706ms 1589ms
优化后 1.84s 0.5s

编译时报错:

打包速度 run build时

类型 首次构建
优化前 1.04m
优化后 59.90s

2. 打包体积

类型 体积大小
优化前 43.5MB
优化后 13.1MB

WebPack 警告WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).解决方案:壳工程使用方案一,关闭webpack自身性能瓶颈提示

javascript 复制代码
// 方式一  webpack中添加如下配置

    performance: {
     
    hints:false   
     
    }
// 方式二 webpack中添加如下配置

    performance: {
        hints: "warning", // 枚举
        maxAssetSize: 30000000, // 整数类型(以字节为单位)
        maxEntrypointSize: 50000000, // 整数类型(以字节为单位)
        assetFilter: function(assetFilename) {
        // 提供资源文件名的断言函数
        return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
        
        }
    },

八、小结

:在小型项目中,添加过多的优化配置,作用不大,反而会因为额外的 loader、plugin 增加构建时间。

在加快构建时间方面,作用最大的是配置 cache,可大大加快二次构建速度。

在减小打包体积方面,作用最大的是压缩代码、分离重复代码、Tree Shaking,可最大幅度减小打包体积。

在加快加载速度方面,按需加载、浏览器缓存、CDN 提升效果显著。

九、依赖检查&依赖可视化

具体可见:juejin.cn/post/723843...

十、静态检查&提交规范

具体可见:juejin.cn/post/723923...

十一、webpack详细配置

webpack.common.js

javascript 复制代码
/*
 * @Author: lzx
 * @Date: 2022-09-13 17:44:56
 * @LastEditTime: 2022-10-08 10:22:03
 * @LastEditors: DESKTOP-LADEI4B
 * @Description: In User Settings Edit
 * @FilePath: \wx20220903\wx-admin\webpack.common.js
 */


var fs = require('fs')
const lessToJs = require('less-vars-to-js')
const path = require('path')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const webpack = require('webpack')
const WebpackBar = require('webpackbar') // 进度条分析
// const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); //编译速度分析
const MomentLocalesPlugin = require('moment-locales-webpack-plugin') // moment按需加载;移除未用到的代码
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin') // lodash按需加载

const pkgJson = require('./package')
const configObj = pkgJson.config['' + process.env.NODE_ENV] || {}
const publicPath = configObj.publicPath
const CDN_BASE = configObj.CDN_BASE

//转换 less 变量,用于主题
const baseLess = lessToJs(
  fs.readFileSync(path.join(__dirname, './src/themes/themes.less'), 'utf8')
)
let themers = {}
Object.keys(baseLess).map((k, i) => {
  let key = String(k).replace(/@/g, '')
  themers[key] = String(baseLess[k])
})
const lessVars = Object.assign(themers, {
  '@CDN_BASE': JSON.stringify(CDN_BASE),
})
console.log('--------' + '当前构建环境是:' + process.env.NODE_ENV + '--------')
console.log('..........' + '开始构建' + '..........')

const isEnvProduction = process.env.NODE_ENV === 'production';
const isDev = process.env.NODE_ENV === 'development'
const commonConfig = {
  entry: {
    app: ['./src/index.js'], // 入口文件
  },
  output: {
    filename: isEnvProduction ? '[name]-v-[fullhash].js' : '[name].js', // 定义输出文件名
    // filename: '[name]-v-[fullhash].js',
    path: path.resolve(__dirname, 'dist'), // 定义输出文件夹dist路径
    publicPath: '',
    // publicPath: isEnvProduction ? 'https://xxx.com' : 'publicPath', // CDN 域名
    chunkFilename: '[name]-v-[fullhash].js',
    pathinfo: false,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './src/index.html'),
      filename: 'index.html',
      favicon: 'favicon.ico',
      CDN_BASE: CDN_BASE,
      publicPath: '',
      inject: 'body', //js插入的位置,插入head中也会自动补defer="defer"属性以保证在页面加载后执行js,如果考虑兼容性可改成body
      // scriptLoading: "defer",
      minify: {
        // 移除空格
        collapseWhitespace: true,
        // 移除注释
        removeComments: true,
      },
    }),
    new webpack.DefinePlugin({
      process: {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'prod:dev'),
      },
      ENV: JSON.stringify(process.env.ENV || 'dev'),
      LANGUAGES_ENV: JSON.stringify(process.env.LANGUAGES_ENV || 'zhCN'),
      CDN_BASE: JSON.stringify(CDN_BASE || ''),
      BACKEND_BASE: JSON.stringify(configObj.BACKEND_BASE),
      publicPath: JSON.stringify(configObj.publicPath),
    }),
    new CopyPlugin({
      patterns: [
        { from: path.resolve(__dirname, './src/assets'), to: 'assets' },
      ],
    }),
    new CleanWebpackPlugin(), // 清除之前的打包文件
    new WebpackBar(),
    new MomentLocalesPlugin(),
    new LodashModuleReplacementPlugin(),
    //全局引入jquery,此后在任何位置可直接使用$,Lodash或其他库也可以像这样引入,当然也可以在dist目录的lib文件夹下放第三方库,在html模板中直接引入
    // new webpack.ProvidePlugin({
    //     '$':'jquery'
    // })
  ],
  cache: {  // 减少二次构建时间
    type: 'filesystem',
    // 默认缓存到 node_modules/.cache/webpack 中
    // 也可以自定义缓存目录
    // cacheDirectory:path.resolve(__dirname,'node_modules/.cac/webpack')
  },
  resolve: {
    alias: {
      pages: path.join(__dirname, 'src/pages'),
      widget: path.join(__dirname, 'src/widget'),
      router: path.join(__dirname, 'src/router'),
      utils: path.join(__dirname, 'src/utils'),
      assets: path.join(__dirname, 'src/assets'),
      themes: path.join(__dirname, 'src/themes'),
      config: path.join(__dirname, 'src/config'),
      'yx-widget': path.join(__dirname, 'src/yx-widget'),
    },
    modules: [path.join(__dirname, 'src'), 'node_modules'],
    extensions: ['', '.js', '.jsx', '.less', '.css'], //配置文件后缀缩小查找范围
    mainFields: ['loader', 'main'], // 从package.json文件中找入口文件。
    mainFiles: ['index.js'],
    symlinks: false, //如不使用npm link 可以设置 为 false,减少解析工作量。
  },
  resolveLoader: {
    // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    // 其中 __dirname 表示当前工作目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')],
  },
  externals: {
    // 'react': 'React',
    // 'react-dom': 'ReactDOM',
    // 'react-router-dom': 'ReactRouterDOM'
  },
  module: {
    rules: [
      {
        test: /.css$/,
        // use表示该文件类型需要调用的loader
        exclude: /node_modules/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            },
          },
          // 'postcss-loader'
        ],
      },
      {
        test: /.less$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
          // isEnvProduction && MiniCssExtractPlugin.loader, // 仅生产环境
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2,
              modules: true,
            },
          },
          // 'postcss-loader',
          {
            loader: 'less-loader',
            options: {
              lessOptions: {
                relativeUrls: false,
                modifyVars: lessVars,
                javascriptEnabled: true,
              },
            },
          },
        ],
      },
      {
        test: /.(png|gif|bmp|jpg)$/,
        type: 'asset/resource',
      },
      {
        test: /.(woff(2)?|eot|ttf|otf|svg|)$/,
        type: 'asset/inline',
      },
      {
        test: /.(js|jsx)$/,
        exclude: /node_modules/, //不解析
        use: [
          {
            loader: 'babel-loader?cacheDirectory=true', // 开启babel-loader缓存',
            options: {
              presets: [
                '@babel/preset-react',
                '@babel/preset-env',
                'react-app',
              ],
              plugins: [
                ['@babel/plugin-proposal-decorators', { legacy: true }],
                '@babel/plugin-syntax-jsx',
                '@babel/plugin-proposal-object-rest-spread',
                'transform-class-properties',
                ['import', { libraryName: 'antd', style: false }],
                [
                  'babel-plugin-imports-transform',
                  {
                    'yx-widget': {
                      transform: 'yx-widget/component/${member}',
                      preventFullImport: true,
                      style: 'yx-widget/component/${member}/index.less',
                    },
                    'yx-widgetDemo': {
                      transform: 'yx-widget/demo/${member}',
                      preventFullImport: true,
                      style: 'yx-widget/demo/${member}/index.less',
                    },
                  },
                ],
                isDev && require.resolve('react-refresh/babel'),
              ].filter(Boolean),
            },
          },
        ],
      },
    ],
  },
}
module.exports = {
  lessVars,
  commonConfig,
}

webpack.dev.config.js

php 复制代码
/*
 * @Author: your name
 * @Date: 2022-09-13 17:44:56
 * @LastEditTime: 2022-10-08 10:22:10
 * @LastEditors: DESKTOP-LADEI4B
 * @Description: In User Settings Edit
 * @FilePath: \wx20220903\wx-admin\webpack.dev.config.js
 */


const { merge } = require('webpack-merge')
const path = require('path')
const { lessVars, commonConfig } = require('./webpack.common.js')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const webpack = require('webpack')
const devConfig = merge(commonConfig, {
    mode: 'development',
    devtool: "eval-cheap-module-source-map", //控制台提示信息映射
    target: 'web',
    // 使用webpck-dev-server时配置
    devServer: {
        contentBase: path.resolve(__dirname, "dist"),
        port: "4098",
        proxy: process.env.ENV === 'loc' ? { //反向代理,根据需求自行修改
            "/viewshine": {
                target: "xxxx",
                changeOrigin: true,
                // pathRewrite: {
                //     "^/viewshine": ""
                //   }
            }
        }:{},
        open: true,
        host: 'localhost',
        hotOnly: false, // 页面构建失败不刷新页面
        hot: true, //让webpackDevServer开启热更新功能
        inline:true,
        // overlay: { warnings: true, errors: true },
        // compress: true,
        // disableHostCheck: true,
        // historyApiFallback: true,
    },
    //如需热更新,开启下面代码
    plugins: [
        // new webpack.HotModuleReplacementPlugin(),
        new ReactRefreshWebpackPlugin({
            overlay:false //该属性是用来在编译出错的时候,在浏览器页面上显示错误。
        }),
    ]
});
module.exports =devConfig

webpack.prod.config.js

php 复制代码
/*
 * @Author: your name
 * @Date: 2022-09-13 17:44:56
 * @LastEditTime: 2023-09-20 13:11:06
 * @LastEditors: lzxaily1107 493535562@qq.com
 * @Description: In User Settings Edit
 * @FilePath: \wx20220903\wx-admin\webpack.prod.config.js
 */


const Path = require("path");
const glob = require('glob');
const { merge } = require('webpack-merge'); //引入配置文件合并工具
// const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const TerserJSPlugin = require("terser-webpack-plugin"); //js压缩
const {commonConfig,less} = require('./webpack.common.js'); //引入公共配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin');// 不兼容weback5
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const BundleAnalyzerPlugin  = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;//打包内容分析
const PATHS = {
  src: Path.join(__dirname, 'src')
};
const RE_VENDOR =/[\/]node_modules[\/](react|react-dom|mobx|mobx-react|mobx-react-router|react-router|react-router-dom|moment|moment/locale/zh-cn|jquery|lodash|axios|js-cookie)[\/]/;

process.env.NODE_ENV === "production";
const prodConfig = merge(commonConfig, {
  mode: "production",
  optimization: {
    runtimeChunk: true,
    moduleIds: 'deterministic', //减少非必要的hash变动
    minimize: true,
    minimizer: [
      //js压缩混稀
      new TerserJSPlugin({
	    	test: /.js(?.*)?$/i,    //匹配参与压缩的文件
	    	parallel: 4,    //使用多进程并发运行
	    	terserOptions: {    //Terser 压缩配置
	    		output:{comments: false}
	    	},
	    	extractComments: true,    //将注释剥离到单独的文件中
	    }),
      // new OptimizeCSSAssetsPlugin({}), //css压缩混稀
      //css压缩
      new CssMinimizerPlugin({
        parallel: 4,
      }),
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups:{
        vendors:{ // node_modules里的代码
          test: /[\/]node_modules[\/]/,
          // test: /[\/]node_modules[\/](react|react-dom)[\/]/,
          // test:RE_VENDOR,
          chunks: "all",
          name: 'vendors', //一定不要定义固定的name
          priority: 10, // 优先级
          enforce: true
        }
      }
    },
    usedExports: true, //开启优化(树摇但保留代码)
    minimize: true  //开启压缩 (删除未使用代码)
  },
  devtool: 'source-map',
  plugins: [
    new CleanWebpackPlugin(),
     // 提取 CSS
    new MiniCssExtractPlugin({
      filename: "css/[name].[contenthash].css" 
    }),
    // 开启css的tree-shaking
    new PurgecssPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true }),
    }),
    new BundleAnalyzerPlugin({
      analyzerMode: "server",
      analyzerHost: "127.0.0.1",
      analyzerPort: 8888, // 运行后的端口号
      reportFilename: "report.html",
      defaultSizes: "parsed",
      openAnalyzer: true,
      generateStatsFile: false,
      statsFilename: "stats.json",
      statsOptions: null,
      logLevel: "info"
    }),
  ],
  //关闭 webpack 的性能提示
  performance: {
    hints:false
  }
});
module.exports =prodConfig

: 某些原因,约束本地升级包管理仅能使用npm

十二、项目地址

codeUp:codeup.aliyun.com/6322fc75d4d...[

](blog.csdn.net/pcaxb/artic...)

视频讲解可前往:www.yuque.com/donsoft/qkn...

相关推荐
老码沉思录1 小时前
React Native 全栈开发实战班 - 数据管理与状态之Zustand应用
javascript·react native·react.js
老码沉思录1 小时前
React Native 全栈开发实战班 :数据管理与状态之React Hooks 基础
javascript·react native·react.js
我认不到你2 小时前
antd proFromSelect 懒加载+模糊查询
前端·javascript·react.js·typescript
凹凸曼打不赢小怪兽5 小时前
react 受控组件和非受控组件
前端·javascript·react.js
鑫宝Code5 小时前
【React】状态管理之Redux
前端·react.js·前端框架
田本初6 小时前
如何修改npm包
前端·npm·node.js
2401_857610039 小时前
深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
前端·javascript·react.js
熊的猫9 小时前
DOM 规范 — MutationObserver 接口
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
fighting ~11 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录11 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js