likes
comments
collection
share

webpack5进阶篇!看完就会辣!

作者站长头像
站长
· 阅读数 18

前言

本篇是 webpack 进阶,以写文章的形式学习、巩固相关知识,如果有幸让您阅读,十分荣幸。

本篇将会通过编译工具开发体验优化打包体积提高构建速率四个方面, 对 webpack 的优化方面的知识进行学习。

您也可以查看我的上一篇文章:webpack基础篇 学习 webpack 的基础配置。

编译工具

编译工具可以提供一些可视化数据,帮助我们分析打包编译的时间、速度、体积等,方便我们针对性的对项目进行优化。

进度条

我们可以通过 progress-bar-webpack-plugin 插件,在打包时,终端显示进度条,方便我们掌握打包的进度。

npm i -D progress-bar-webpack-plugin

配置如下:

//webpack.config.common.js
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)`,
        }),
    ]
}

当我们如上配置后,执行 npm run build 时,就会看见进度条辣!

webpack5进阶篇!看完就会辣!

分析打包速度

打包时,可能会想知道所使用的插件、loader的编译时长,然后针对性的优化,提高构建速率时,我们可以通过 speed-measure-webpack-plugin 插件来实现。

npm install speed-measure-webpack-plugin -D

配置如下:

//webpack.config.common.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); 
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
    // webpack 配置
})

这样,在我们打包时,会看见终端显示插件、loader等工具的打包时间:

webpack5进阶篇!看完就会辣!

分析打包体积

在对项目进行优化的时候,可能会去拆分某些模块,但是又不清楚这些模块的体积,我们就可以通过 webpack-bundle-analyzer 这个插件来辅助我们分析 bundle.js。

npm install webpack-bundle-analyzer -D

配置如下:

//webpack.config.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 

module.exports = { 
    plugins: [ 
        new BundleAnalyzerPlugin()
    ], 
}

执行 npm run build 后,会自动弹出一个页面:

webpack5进阶篇!看完就会辣!

为我们提供可视化界面,方面我们对打包后的文件模块体积分析。

开发体验

热更新

什么是热更新?热更新就是浏览器不刷新,在不改变页面组件状态的情况下仅更新修改的部分。 而在 webpack5 中,开发环境的时候,webpack-dev-server 插件可以帮助我们配置热更新。

npm install webpack-dev-server -D

配置如下:

//webpack.config.dev.js
module.exports = {
    devServer: {
        hot: true //开启热更新
    }
}

但仅仅上面配置可能还不够哦!

在实际项目开发过程中,我们修改 less、sass 样式文件时,可以不刷新浏览器的情况下生效,这是因为 less、sass 最终会通过 style-loader 处理,而 style-loader 实现了热模块更新的功能

但是当我们在开发(比如 React 项目)时,在 Input 组件输入内容后,修改 index.tsx 后,浏览器会刷新,然后输入框的内容就不会保留,这不是我们希望的。

所以这时候需要 react-refresh-webpack-pluginreact-refresh插件

npm install @pmmmwh/react-refresh-webpack-plugin react-refresh -D

配置如下:

//webpack.config.dev.js 
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

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

在 babel.config.js 文件中配置 react-refresh

//babel.config.js
const isDev = process.env.NODE_ENV === 'development';

module.exports = {
    "plugins": [ 
        //开发模式下,开启 react 热更新
        isDEV && require.resolve('react-refresh/babel)
    ].filter(boolean)
}

sourceMap

souceMap 是将 源代码与打包后代码 建立映射的配置。它会生成一个 .map 文件,里面包含源代码和构建后代码的映射关系。这样当代码出错时,我们可以找到问题所在。

souceMap 的配置有多种,区别如何这里就不多说(因为我也不搞不太清也记不住,如果有好的见解、文章,欢迎在评论区评论!)

就我个人而言,我一般是这样配置的:

  • 开发模式:cheap-module-source-map

    • 优点:打包编译速度快
  • 生产模式:source-map

    • 优点:源代码与构建后代码一一映射

配置如下:

//webpack.config.dev.js 
module.exports = {
    devtool: "cheap-module-source-map",
}

//webpack.config.prod.js
module.exports = {
    devtool: "source-map",
}

提高构建速率

cache缓存

webpack 通过配置缓存,会在第一次打包后,缓存文件,当后续打包时,极大的提高构建速率。

配置如下:

//webpack.config.common.js
module.exports = {
    cache: {
        //文件缓存
        type: 'filesystem',
    }
}

执行 npm run build 后,可以在 node_modules/.cache/webpack 文件夹下发现一个 default-production 文件夹

webpack5进阶篇!看完就会辣!

同样,当执行 npm run serve 执行运行环境时,在该文件夹下也会生成一个 default-development

alias 别名

通过配置 resolve.alias 别名的方式,减少引用文件的路径复杂度

配置如下:

module.exports = {
    resolve: {
        alias: {
            //把 src 文件夹别名为 @
            //引入 src 下的文件就可以 import xxx from '@/xxx'
            '@': path.join(__dirname, '../src')
        }
    }
}

减少 loader 作用范围

我们可以通过 excludeinclude 来减轻 loader 的作用范围,提高构建速度

  • include:只解析该配置项的模块
  • exclude:排除该配置项的模块

配置如下:

//webpack.config.common.js
module.exports = {
    module: {
        rules: [
            {
                test: /\.(|ts|tsx)$/,
                // 只解析 src 文件夹下的 ts、tsx 文件
                // include 可以是数组,表示多个 文件夹下的 模块都要解析
                include: path.resolve(__dirname, '../src'), 
                use: [ 'thread-loader', 'babel-loader']
            }
        ]
    }
}

其他的 loader 也可以这样配置减少作用范围。

保证 loader 准确性

loader 在打包过程中,会遍历 rules 数组,根据文件后缀名是否和 test 匹配,使用对应的 loader 来解析文件。

而在这个过程中,应该避免使用不必要的 loader。如使用 less-loader 去解析 css。

配置如下:

module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        include: [path.resolve(__dirname, '../src')],
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /.less$/,
        include: [path.resolve(__dirname, '../src')],
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
    ]
  }
}

其他的也是如此,比如 ts、tsx。

extensions

我们在项目中 import 文件时,有时候希望不想去管到底是什么后缀的文件时,可以配置 resolve.extensions ,这样打包时,webpack 会按照我们配置的 extensions 规则去查找对应的文件后缀。

module.exports = {
    resolve: {
        //extensions 是从左往右解析,所以使用频率高、重要的文件后缀请写在前面
        extensions: ['.tsx', '.ts', '.less']
    }
}

modules

在项目中,我们会通过 import 大量引入模块,而模块又分为

  • node 核心模块,比如 const path = require('path')
  • node_modules 模块
  • 自定义模块,比如 API 接口模

在引入模块的时候,会以 node 核心模块 -----> node_modules ------> node全局模块 的顺序查找模块。而我们通过配置 resolve.modules 指定 webpack 搜索的范围,快速找到相关模块。

配置如下:

const path = require('path');

module.exports = {
    resolve: {
        modules: [path.resolve(__dirname, '../node_modules')]
    }
}

开启多进程

webpack 的 loader 默认是单线程执行的,而现在的电脑一般都是多核的,所以可以通过开启多进程,帮助 loader 进行解析,减少编译时间,提高构建速率。

我们可以通过 thread-loader 来实现。

npm install thread-loader -D

配置如下:

module.exports = {
    modules: {
        rules: [
            {
                test: /(\.jsx|\.js|\.ts|\.tsx)$/,
                use: ['thread-loader', 'babel-loader']
            }
        ]
    }
}

注意:开启多进程也需要时间。

优化打包体积

在 webpack 打包后,会得到一个 bundle.js 文件,这个文件就是页面执行的脚本,如果 bundle.js 体积过大,页面展现的速率可能就慢,影响用户体验,所以这也是很重要的优化方面。

压缩 JS

打包时,我们可以使用 webpack5 里面自带一个插件 terser-webpack-plugin 来压缩 JS 代码。

//webpack.config.prod.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({ // 压缩js
        parallel: true, //开启多进程
        //更多配置请看官网
      }),
    ],
  },
}

压缩 CSS

我们可以通过 css-minimizer-webpack-plugin 插件来实现。

npm install css-minimizer-webpack-plugin -D

配置如下:

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ],
  }
}

压缩图片

我们可以通过 image-minimizer-webpack-plugin 插件来实现图片压缩

npm install image-minimizer-webpack-plugin -D

//webpack.config.prod.js
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");

module.exports = {
  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",
                      },
                    },
                  ],
                },
              ],
            ],
          },
        },
      }),
    ],
  },
};

抽离 css 代码

我们想将 css 单独打包成一个文件,可以使用 mini-css-extract-plugin 插件。

npm install mini-css-extract-plugin -D

配置如下:

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

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

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: getStyleLoaders(),
      },
      {
        test: /\.less$/, 
        use: getStyleLoaders("less-loader"),
      },
      {
        test: /\.s[ac]ss$/,
        use: getStyleLoaders("sass-loader"),
      }
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
};

执行 npm run build 后,dist 目录下就会生成单独的文件 main.css

webpack5进阶篇!看完就会辣!

splitChunk 代码分割

splitChunk 可以帮助我们进行代码分割。

  • 比如 第三方库的代码,一般改动很小,就可以单独打包成一个 chunk;
  • 比如 公共模块,提取出公共的避免多次打包增加 bundle.js 的体积;

配置如下:

module.exports = {
  // ...
  optimization: {
    // ...
    splitChunks: { // 代码分离
      cacheGroups: {
        vendors: { 
          test: /node_modules/, // 把 node_modules 第三方模块拆成单独 chunk
          name: 'node_modules.vendors', // 名字是 node_modules.vendors
          minChunks: 1, // 至少用一次就切出来
          chunks: 'all', // 同步异步的都提取出来
          priority: 1, // 优先级
        },
        commons: { // 公共代码拆成单独的 chunk
          name: 'commons', // 名字是 commons
          minChunks: 2, // 至少用两次就提取出来
          chunks: 'all', // 同步异步的都提取出来
          minSize: 0, // 提取代码体积大于0就提取出来
        }
      }
    }
  }
}

合理配置文件 hash 值

我们可以通过配置文件的 hash 值,来缓存文件,减少服务器压力,提高页面渲染。

hash 值分为三种:

  • hash:跟整个项目有关,只要项目里面有一个文件改变了,则重新生成,且全部文件都用同一个hash
  • chunkhash:对应模块的 hash 值,修改对应 chunk 本身、或 chunk 的依赖改变,才会重新生成。
  • contenthash:文件自己的 hash 值,文件改动只会影响自己的 hash 值。

配置时,一般是 js 文件以 chunkhash 结尾,图片、css 以 contenthash 结尾

配置如下:

// webpack.config.common.js
module.exports = {
  output: {
    //js用 chunkhash
    filename: 'static/js/[name].[chunkhash:8].js', 
  },
  module: {
    rules: [
      {
        test:/.(png|jpg|jpeg|gif|svg)$/,
        generator:{ 
          //图片用 contenthash
          filename:'static/images/[name].[contenthash:8][ext]'
        },
      },
      {
        test:/.(woff2?|eot|ttf|otf)$/, 
        generator:{ 
          //字体用 contenthash
          filename:'static/fonts/[name].[contenthash:8][ext]',
        },
      },
      {
        test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/,
        generator:{ 
          //媒体文件用 contenthash
          filename:'static/media/[name].[contenthash:8][ext]',
        },
      },
    ]
  },
}

tree-shaking

tree-shanking 其实就是在打包的时候,把引用了但没使用的模块给清除掉

JS tree-shaking

webpack5 在 production mode 时,自动开启 tree-shaking

CSS tree-shaking

在 css 里面,也会存在我们写了,但是没有使用的样式,这部分样式对于打包的文件来说就是多余的,得清除掉。

我们可以通过 purgecss-webpack-plugin 来实现。

npm install purgecss-webpack-plugin -D

配置如下:

const glob = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const paths = require('paths');

module.exports = {
  plugins: [
    // CSS 单独打包成一个 文件
    new MiniCssExtractPlugin({
      filename: "asset/css/main.css",
    }),
    // CSS Tree Shaking
    new PurgeCSSPlugin({
      paths: glob.sync(`${paths.appSrc}/**/*`,  { nodir: true }),
    }),
  ]
}

懒加载

虽然我们前面通过 splitChunk 进行了代码分离,但是 react、vue 默认会把代码全部打包到 bundle.js 中,而在首页得时候,其实我们只需要首页得代码就行了,其余得等需要加载时再加载即可,可以提高渲染速度。

webpack5 里面默认支持懒加载,通过 import 语法实现。

//在 React 项目中:

//src/component/A.tsx
const A = () => {
    return (
        <div> AAAA </div>
    )
}
export default A;


//src/component/B.tsx
const B = () => {
    return (
        <div> BBB </div>
    )
}
export default B;


//App.tsx
import React, { lazy } from 'react';
const A = Lazy(() => import('@/component/A.tsx'));
const B = Lazy(() => import('@/component/B.tsx'));

function App() {
    return (
        <div>
           //引入了 A、B 子组件,但是只使用了 A 组件
           <A />
        </div>
    )
}

export default App

在打包后,bundle.js 里面只有 A 组件的内容,不会有 B 组件的。

预加载

预加载可以通过 preLoadpreFetch 来实现。

  • preload:告诉浏览器,当前页面必须加载的资源,即一定会去加载这些资源
  • prefetch:告诉浏览器,当前页面可能需要的资源,不一定现在需要,有空时去加载。

在 React 项目中,我们可以通过 import + webpack 魔法注释实现。

// src/components/A.tsx
import React from "react";
function A() {
  return (
      <div> AAA </div>
  )
}
export default A

// src/components/B.tsx
import React from "react";
function A() {
  return (
      <div> BBB </div>
  )
}
export default B


// App.tsx
import React, { lazy, Suspense, useState } from 'react'
// prefetch
const A = lazy(() => import(
  /* webpackChunkName: "preFetchComponent_A" */
  /* webpackPrefetch: true*/
  '@/components/A'
))
// preload
const B = lazy(() => import(
  /* webpackChunkName: "preLoadComponent_B" */
  /* webpackPreload: true*/
  '@/components/B'
 ))

function App() {
  const [ visible, setVisible ] = useState(false)

  const onClick = () => {
    setShow(true)
  }
  return (
    <>
      <div onClick={onClick}>show</div>
      { visible && (
        <>
          <A />
          <B />
        </>
      ) }
    </>
  )
}
export default App

打包后,页面初始化的时候就会预加载 A 组件的资源,直接加载 B 组件的资源。当点击了 show 之后,A组件中的代码才执行

结尾

以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论。