《Webpack 配置指南:从基础到高阶专栏》- 高阶篇

886 阅读10分钟

前言:

继前一篇《Webpack配置指南:从基础到高阶篇-基础篇》 之后的后续篇,整理了一些webpack的打包优化、兼容性处理等方面的配置方法。涵盖: 代码分割什么是preload和prefetchruntimeChunks的缓存优化将antd等不频繁更新的第三方依赖通过dll打包使用oneof配置loader加载优化eslint和babel相关的缓存配置PWA多进程程打包等

本文源码:webpack渐进式配置指南逐渐整理中 如果本文帮到了你,一个赞❤️是鼓励我持续创作的莫大动力❤️☕️ 如果有问题,欢迎评论区交流讨论~

打包编译过程优化

⭐️代码分割

代码分割通常有三种情况:

入口entry手动分割

这个比较好理解,就是多入口配置,但是对于我们的SPA项目,可发挥的空间不大。

动态导入按需加载

  • 按需加载

可以将初始化并不需要的模块在特定事件中按需加载,这样只会在特定事件触发时请求加载,减少首屏渲染时间。

image.png

  • 魔法命名与chunkFilename

此外,我们还可以在动态import的时候,对我们异步加载的模块进行“魔法命名”,如下:

一定要注意格式,名称单双引号都可以,但是webpackChunkName后面的冒号可不能丢。

image.png

然后我们就可以在webpack的output中配置chunkFilename取到这个名字。

image.png

来波“魔法操作”,看看实际打包输出的chunk结果。 image.png

如果我们不自定义命名,webpack也会替我们取一个有意义的名称,可以看到是结合路径命名的,而且更准确还可以防止命名冲突。

image.png

去重和分离chunks

采用webpack的optimization中的splitChunks,将其chunks配置设置为all(默认是async,只会对异步代码进行分割,设为all会将同步异步一起按配置分割),而且使用splitChunks还可以对重复引用的模块进行单独打包避免重复打包,提升打包性能

image.png

Preload和Prefetch预加载和预获取

这个比较重要,单独展开解释一下。

Preload和Prefetch

为什么用:

我们在使用动态导入按需加载做代码分割时,如果异步加载的代码体积比较大,就会出现明显卡顿情况,用户体验不好。那可不可以在浏览器空闲的时候帮我们加载一下可能使用到的资源呢?

什么是

  • Preload :告诉浏览器立即加载
  • Prefetch :告诉浏览器空闲时加载

共同点

  • 他们都会提前加载资源,但不会执行
  • 都有缓存

区别

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

存在问题

兼容性都比较一般

  • 可以使用 Can i use 查看各自的兼容性问题
  • preload相对于prefetch兼容性能好些。

具体用法:

我们可以使用@vue/preload-webpack-plugin这个插件帮我们实现上述需求。

安装

注意:官方插件npm文档给出的安装方式包名不正确

image.png preload-webpack-plugin是谷歌实验室的一个包,使用跟这个一样,感兴趣的自己研究一下。此处我们使用vue团队开发的这个插件实现。

pnpm add @vue/preload-webpack-plugin -D

使用:

const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
// 在plugins数组中新增如下配置new PreloadWebpackPlugin({
    rel: 'preload',
    as: 'script' // script标签的优先级基本满足需求,如果更高可以使用style
  })//or
 new PreloadWebpackPlugin({
    rel: 'prefetch'
  }) 

上述配置的原理:以preload配置为例,会在入口html中生成一个link标签,设置其rel为preload,as为script。

image.png

runtimeChunks缓存优化

runtimeChunks的主要作用是将 Webpack 的运行时代码从应用的业务代码中分离出来,提取到单独的 chunk 中。运行时代码包括模块的加载逻辑、解析模块之间依赖关系的代码等通过将运行时代码分离出来,可以更好地利用浏览器缓存。当业务代码中的某个模块(如 B 模块)发生变化时,只有该模块对应的 chunk 以及可能被影响的其他相关 chunk 会被重新打包,而运行时代码 chunk 通常不会发生变化(除非 Webpack 的运行时逻辑有重大更新)。这样,浏览器可以继续使用缓存的运行时代码 chunk,只需要重新下载变化的业务代码 chunk,从而提高应用的加载速度。

将不会频繁更新的静态文件通过dll自定义打包

以qiankun+umi为架构为例,可以将类似antd这种第三方库包通过dll打包插件提前打好,然后维护一份模块清单,给在需要使用的子应用通过配置manifest,指向其维护的清单,从而子应用重复打包造成的性能损耗,包括降低打包效率,具体操作如下:

  • 在主应用的webpack中配置生成模块清单

在webpack.dll.config.js中新增如下配置,用到webpackDllPlugin插件,path是模块清单的输出路径,生成的清单文件通常是易于读取的json格式,name是对应模块的资源名称。

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

const version = '20240220';

module.exports = {
  entry: {
    vendor: [
      'react',
      'react-dom',
      'react-highcharts',
      'react-redux',
      'react-router',
      'react-router-dom',
      'react-router-redux',
      'redux',
      'xlsx/dist/xlsx.full.min.js',
      'antd',
      // 'bizcharts',
      'classnames',
      // 'd3',
      // 'dagre-d3',
      // 'echarts',
      // 'echarts-for-react',
      // 'g2',
      // 'g2-plugin-slider',
      'highcharts',
      'highcharts-custom-events',
      'highcharts-react-official',
      'js-cookie',
      // 'js-export-excel',
      // 'lodash',
      'moment',
      // 'qs',
    ],
  },
  output: {
    path: path.resolve(__dirname, './web/vendor'),
    filename: `[name]-${version}.js`,
    library: 'dll_[name]_[hash]',
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.resolve(__dirname, './web/vendor', `[name]-manifest-${version}.json`),
      name: 'dll_[name]_[hash]',
    }),
  ],
};
  • 子应用中新增如下配置,使得子应用在能按配置路径读取到对应静态资源的同时,不会重复打包。

用到的插件是DllReferencePluginmanifest是对应的清单资源路径。

if (process.env.DLL !== 'none') {
    config.plugin('dll').use(webpack.DllReferencePlugin, [
      {
        context: path.resolve(__dirname, '../../../'),
        manifest: require('../../../web/vendor/vendor-manifest-20220623.json'),
      },
    ]);
  }

使用oneof提升打包优化效率

webpack的loader配置如果不做优化的话,默认会遍历rules中的所有loader配置,直至找到解析特定文件的合适的loader,这种机制显然没有必要,因此我们可以使用oneof优化打包速率。

image.png

开启eslint和babel配置缓存

【开启babel缓存】

  • 下载安装
pnpm add babel-loader @babel/core @babel/preset-env -D
  • 新建.babelrc配置文件

也可以在babel-loader中直接写,但是通过配置更加灵活

module.exports = {
  // babel面向未来的JavaScript语法,将其转换为当前浏览器支持的语法
  presets: ["@babel/preset-env"],
};
  • 配置babel-loader
{
    test: /\.(js|ts)$/,
    include: path.resolve(__dirname, "../src"),
    loader: "babel-loader"
}
  • 开启babel缓存
 {
    test: /\.(js|ts)$/,
    include: path.resolve(__dirname, "../src"),
    loader: "babel-loader",
    options: {
      cacheDirectory: true, // 开启babel缓存
      cacheCompression: false, // 关闭babel缓存压缩,提升编译速度
      plugins: ["@babel/plugin-transform-runtime"], // 减少babel编译代码体积
    },
  },

开启后,会在node_modules的.cache文件下生成对应缓存文件,避免重复处理.

image.png

【开启eslint插件缓存】

默认好像已开启,如果版本不默认开启可手动开启。

image.png

减少babel-loader产物体积

babel为每个文件都插入了辅助代码,比如_extend等导致体积过大,可以将这些辅助函数单独抽离成一个模块避免重复引入。更多参考。用到 @babel/plugin-transform-runtime插件

● 下载安装

pnpm add @babel/plugin-transform-runtime -D
pnpm add @babel/runtime

● 打包配置

image.png

tree-shaking

webpack已默认支持

开启多进程打包

什么是

开启电脑的多个线程执行打包任务,工作在线程池中,工程量越大时效益越明显,但是需要特别注意:请在特别耗时的情况下才考虑使用,单个进程只启动就有大约600ms的开销,官方传送门

怎么用

我们启动进程的数量就是CPU的核数,具体操作如下:

  • 获取CPU的核数(每个电脑都不一样)
const os = require("os");
// 获取CPU核数
const threads = os.cpus().length;
  • 下载包
pnpm add thread-loader -D
  • 配置使用

先来给babel-loader加一下,给babel-loader加的时候,一定注意要放在其前面(loader的执行顺序是从下往上或者可以说从右往左)。

{
    test: /.(js|ts)$/,
    include: path.resolve(__dirname, "../src"),
    use: [
      {
        loader: "thread-loader", // 开启多线程loader
        options: {
          workers: threads, // 开启的线程数
        },
      },
      {
        loader: "babel-loader",
        options: {
          cacheDirectory: true, // 开启babel缓存
          cacheCompression: false, // 关闭babel缓存压缩,提升编译速度
          plugins: ["@babel/plugin-transform-runtime"], // 减少babel编译代码体积
        },
      },
    ],
  },

再给Eslint插件-EslintWebpackPlugin配置一下

// 配置eslint插件
    new EslintWebpackPlugin({
      context: path.resolve(__dirname, "src"),
      cache: true, // 开启eslint缓存
      threads, // 开启多线程eslint
    }),

再给JS压缩插件-TerserWebpackPlugin配置一下(在optimization下面哈)

 minimizer: [
  // 压缩css
  new CssMinimizerPlugin(),
  // 压缩js
  new TerserWebpackPlugin({
    parallel: true, // 开启多线程压缩
    terserOptions: {
      compress: {
        drop_console: true, // 删除console
        drop_debugger: true, // 删除debugger
      },
    },
  }),
],

PWA(Progressive web application)

官方术语:渐进式网络应用程序,用人话说就是:提供离线访问页面的能力!

怎么做?

○ 下载插件: pnpm add workbox-webpack-plugin -D

○ 配置webpack打包

const WorkboxPlugin = require('workbox-webpack-plugin');
在webpack.config.js或者其他webpack配置文件的plugins中新增如下配置:
 // PWA插件,用于生成service-worker文件,提供离线访问能力
new WorkboxPlugin.GenerateSW({
  clientsClaim: true, // 立即激活service
  skipWaiting: true, // 强制等待中的service worker被激活
}),

在需要激活的入口(比如index.js/main.js)加入service-worker相关代码

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);
      });
  });
}

重新打包后可以看到生成了service-worker相关的代码。

image.png

然后我们需要运行打包后的文件观察离线缓存情况,可以在本地快速启动一个服务(比如serve、http-server、live-server都可以),我以serve为例,全局下载安装serve,然后执行serve lib(打包生成目录),可以看到控制台已经可以看到我们注册的service workder了,断网刷新也实现了缓存能力。

image.png

image.png

缓存数据可以通过Cache storage中查看。

image.png

处理浏览器兼容性问题

通常一些诸如flex在个别浏览器中存在兼容性问题,那么可以通过postcss处理,过程如下:

  • 下载安装
 pnpm add postcss-loader postcss postcss-preset-env -D
  • 配置webpack打包

注意:我们需要放在css-loader后面,less-loader等loader的前面。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = (pre) => {
  return [
    // 使用MiniCssExtractPlugin.loader替换style-loader,可以提取css到单独文件
    MiniCssExtractPlugin.loader,
    "css-loader",
    {
      // 通过postcss-loader自动处理浏览器兼容性
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: [
            [
              "postcss-preset-env",
              {
                browsers: "last 2 versions",
              },
            ],
          ],
        },
      },
    },
    pre, // 动态传入预处理器的loader,注入less-loader/sass-loader/stylus-loader
  ].filter(Boolean);
};

项目源码:webpack渐进式配置指南逐渐整理中

如果本文帮到了你,一个赞❤️是鼓励我持续创作的莫大动力❤️☕️ 如果有问题,欢迎评论区交流讨论~

其它专栏文章推荐:

《手撕源码系列专栏》文章

《重学JavaScript专栏》相关文章推荐: