Webpack SplitChunksPlugin插件研究

SplitChunksPlugin初见

自Webpack4之后,代码拆分的插件由CommonsChunkPlugin变成了SplitChunksPlugin,而且没必要单独引用,集成到了Webpack之中,经过配置下的optimization.splitChunks和optimization.runtimeChunk就能够控制了。vue

研究插件经验

前两天研究了一下CommonsChunkPlugin插件,总结出来一条经验,就是要理解这个插件,单纯看如何配置它,是不会懂它的。node

先知道它的设计思路,再学习如何配置它。react

CommonsChunkPlugin的不足

Webpack4的代码拆分方案,彻底换了一个插件,想必设计思路和使用上差异会比较大,实际上也的确如此。webpack

若是是一些简单的重复代码的拆分,CommonsChunkPlugin是能够胜任的。但一些复杂的场景,CommonsChunkPlugin就不行了。web

复杂场景举例

咱们的项目结构是这样的:浏览器

  • 有一个入口文件: index.js
  • 三个用于异步加载的文件:Greeter1.js、Greeter2.js、Greeter3.js
  • Greeter1.js、Greeter2.js引用了React.js,Greeter3.js 引用了Vue.js

使用CommonsChunkPlugin的状况

这时候用Webpack打包此项目,使用CommonsChunkPlugin的话,会将React.js Vue.js这些库打包到vendor.js中。缓存

这样作的问题:bash

  1. 咱们获得了一个很是大的公共Chunk。浏览器加载咱们的项目首屏时,会加载入口Chunk index.js和公共Chunk vendor.js。这样对首屏加载速度是不利的。
  2. 某用户只是看Greeter1.js和Greeter2.js这两个Chunk的内容,那么对于他来讲,加载Vue.js这样根本用不到的库,只是浪费流量。

理想的拆分和使用状况

不是将React.js、Vue.js打包到同一个vendor Chunk中,而是Webpack经过分析,将React.js打包到一个vendor~Greeter1~Greeter2.js中,将Vue.js打包到一个vendor~Greeter3.js中,这样分别打包公共代码。并发

而后首屏加载的时候,只加载入口Chunk index.js。等用户查看Greeter1.js的时候,再并行加载Chunk Greeter1.js和Chunk vendor~Greeter1~Greeter2.js。查看Greeter3.js的时候,再并行加载Chunk Greeter3.js和Chunk vendor~Greeter3.js。app

这样,解决了上面提到的两个问题,首屏速度和流量浪费。

使用SplitChunksPlugin解决

SplitChunksPlugin就是能够应付上面描述的复杂的拆分状况,比较理想的拆分代码。

搭建实验项目

按照上面描述的,咱们新建文件,目录结构以下:

image

文件内容以下:

// index.js 
// 这样就是异步加载Greeter一、二、3 三个Chunk

import(/* webpackChunkName: "Greeter1" */'./Greeter1').then(module => {
  const greeter = module.default
  document.querySelector("#root").appendChild(greeter());
})

import(/* webpackChunkName: "Greeter2" */'./Greeter2').then(module => {
  const greeter = module.default
  document.querySelector("#root").appendChild(greeter());
})

import(/* webpackChunkName: "Greeter3" */'./Greeter3').then(module => {
  const greeter = module.default
  document.querySelector("#root").appendChild(greeter());
})
复制代码
// Greeter1.js / Greeter2.js
// 咱们就这样模拟引用了React。Greeter2.js和Greeter1.js长得同样。

import React from 'react';
console.log(React);

export const greeter = function () {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};
复制代码
// Greeter3.js
// Greeter3中引用的是Vue

import Vue from 'vue';
console.log(Vue);

export const greeter = function () {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};
复制代码

再来看看关键的Webpack配置

module.exports = {
  entry: {
    index: __dirname + "/app/index.js",
  },
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "[name].js", //打包后输出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
        },
      }
    }
  },
}
复制代码

关键的就是optimization下的配置,咱们先只关注cacheGroups下的vendors配置。其中的test是分割代码的规则,表明node_modules文件夹下的代码都要被抽离出来。咱们运行一下Webpack,看看输出结果:

image

我查看了一下vendor~Greeter1~Greeter2.js文件,里面是React打包后的代码。 我查看了一下vendor~Greeter3.js文件,里面是Vue库打包后的代码。

再利用打包分析,看到:

image

这不就是咱们设想的那种理想状况。若是是CommonsChunkPlugin,配置后,只会帮咱们打包出一个vendor.js的公共Chunk,而SplitChunksPlugin,咱们只是告诉它node_modules下的文件要抽离出来,Webpack就根据项目的引用状况,自动分理处两个公共Chunk vendor~Greeter1~Greeter2.js 和 vendor~Greeter3.js

SplitChunksPlugin设计思路总结

解决复杂场景下的代码拆分问题。针对异步加载中公共模块的拆分,咱们只需设置须要被公共打包的代码,SplitChunksPlugin就会自动帮咱们按照各异步模块的需求,将公共的Chunk拆分红一些小的公共Chunks。供各异步模块使用。而且这些公共Chunks不会首屏加载,会随着使用使用它们的异步模块,使用时再一同并行加载。

核心思路:根据咱们给的规则拆分代码,而后针对拆分的公共Chunk,再次拆分。

拆分出来的Chunk过多,怎么办

到这里,还需一种极端状况,就是被拆分出来的公共Chunk,太多了。Webpack的初衷是合并代码啊,这又给拆碎了。

过多Chunk致使的问题就是浏览器同时须要并发请求太多的js。

一样的SplitChunksPlugin也替咱们想到了。

咱们再来作一个实验。

复杂场景举例:

  • 有一个入口文件: index.js
  • 有四个用于异步加载的文件:Greeter1.js、Greeter2.js、Greeter3.js、Greeter4.js。
  • 有三个给上四个文件引用的文件:helper1.js、helper2.js、helper3.js(注意这里helper*.js都要大于30k,过小了Webpack不会将其抽离出一个Chunk)。
  • Greeter1.js引用helper1.js,Greeter2.js引用helper2.js,Greeter3.js引用helper3.js,,Greeter4.js同时引用helper1.js、helper2.js、helper3.js。

我画一个图说明状况:

image
G1表明Greeter1.js,h1表明helper.js。依此类推。这样的引用,造成了helper1~3都成了公共模块。须要咱们将其提取出Chunk。使用Wepback编译,查看结果:

image

是否跟你预想的同样,出现了三个公共Chunk,也就是咱们上图画的公共部分,分别包含了header1~3的代码。再看更直观的效果图:

image

Greeter4.js的问题

浏览器加载Greeter4.js的时候,须要同时加载default~Greeter1~Greeter4.js、default~Greeter2~Greeter4.js、default~Greeter3~Greeter4.js三个Chunk。也就是用户看Greeter4.js时,需并行请求4个js文件。

问题就在于咱们把公共包拆的过于细,有可能会出现,加载一个异步Chunk的时候,须要同时而且请求不少的公共Chunk,这不是咱们想看到的,为此,SplitChunksPlugin提供给咱们一个属性maxAsyncRequests,限制最大并行请求数。

目前的最大的并行请求数是加载Greeter4.js时的4,咱们设置成3,看看什么效果:

optimization: {
    splitChunks: {
      maxAsyncRequests: 3, // 在此设置
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },
复制代码

运行webpack,效果以下:

image

Greeter4将helper3.js打到一个Chunk里,而后helper一、helper2单独打包,这样Greeter4的并行请求数等于3,符合预期。

一样的将helper3.js一样被Greeter3.js引用,因此也打包到了Greeter3中,形成了重复打包helper3。为了减小而且请求数,就会致使必定程度的重复打包,咱们要作的,就是经过配置在平衡而且请求数和重复打包率上作一个平衡。

总的来讲SplitChunksPlugin仍是很智能啊,咱们只是提出要求(并行请求数要小于等于3),它就会基于此条件为咱们的进行拆包和组合包。

SplitChunksPlugin默认配置

即便咱们不写optimization,Webpack也会帮助咱们进行代码拆分,至关于咱们写了以下的配置:

splitChunks: {
    chunks: "async", // 默认只处理异步chunk的配置
    minSize: 30000, // 若是模块的最小体积小于30,就不拆分它
    minChunks: 1, // 模块的最小被引用次数
    maxAsyncRequests: 5, // 异步加载Chunk时的最大并行请求数
    maxInitialRequests: 3, // 入口Chunk的最大并行请求数
    automaticNameDelimiter: '~', // 文件名的链接符
    name: true, // 此处写成false,公共块就不会是default~Greeter1~Greeter4.js了,而是0.js这样命名Chunk。
    cacheGroups: { // 缓存组,拆分Chunk的规则
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10, // 此数越大,越优先匹配
        },
        default: {
            minChunks: 2, // CommonsChunkPlugin的minChunks既能够传方法,也能够传数字,如今只能够传数字了,若是你想传方法,用test属性
            priority: -20,
            reuseExistingChunk: true //  配置项容许重用已经存在的代码块而不是建立一个新的代码块。这句我不懂,有知道的小伙伴麻烦告诉我一下
        }
    }
}
复制代码

能够看到默认配置只对异步加载的Chunk有效,缘由是配置了 chunks: "async"。

如下是默认配置的描述:

  • 被共享的代码块或者来自node_modules文件夹
  • 被分割的代码块要大于30kb(在min+giz以前)
  • 按需加载代码块的请求数量应该<=5
  • 页面初始化时加载代码块的请求数量应该<=3

这些描述分别对应了上面哪条配置,相信你们都清楚了。若是没有通过分析,这些描述真是让人摸不着头脑。

maxInitialRequests

maxInitialRequests字段咱们尚未解释,看字段名字应该是初始化时,也就是针对入口Chunk的分割吧,因而我作了以下配置:

module.exports = {
  entry: {
    Greeter1: __dirname + "/app/Greeter1.js",
    Greeter2: __dirname + "/app/Greeter2.js",
    Greeter3: __dirname + "/app/Greeter3.js",
    Greeter4: __dirname + "/app/Greeter4.js",
  },
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "[name].js", //打包后输出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  optimization: {
    splitChunks: {
      chunks: "initial", // 默认只处理异步chunk的配置
      maxInitialRequests: 3, // 一个入口最大并行请求数
      cacheGroups: { // 缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },
}
复制代码

咱们来看打包效果:

image

我将maxInitialRequests调成5,再来看打包效果:

image

打包的结果,和咱们分析异步Chunk的提取策略一致,限制为5的时候,即便是Greeter4.js的最大而且请求数才是4,因此能够尽情的拆包。但限制为3的时候,Webpack就不把helper3.js单独拆成一个公共Chunk了,而是分别打包到引用了它的Greeter4.js和Greeter3.js里,以此来限制Greeter4这个入口Chunk被加载时,并行请求为3。能够说maxInitialRequests就是 针对多入口限制拆包数量的maxAsyncRequests。

拆分runtime的代码

说了这么多,尚未提到拆分runtime呢。SplitChunksPlugin拆分runtime只需配置一个属性,以下:

optimization: {
    runtimeChunk: true,
    splitChunks: {
      chunks: "initial", // 默认只处理异步chunk的配置
      minSize: 30000, // 若是模块的最小体积小于30,就不拆分它
      minChunks: 1, // 模块的最小被引用次数
      maxAsyncRequests: 5, // 按需加载的最大并行请求数
      maxInitialRequests: 5, // 一个入口最大并行请求数
      automaticNameDelimiter: '~', // 文件名的链接符
      name: true,
      cacheGroups: { // 缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },
复制代码

我这里仍是沿用上一个例子,打包结果以下:

image

颇有意思,针对咱们四个入口文件,分别生成了四个文件,runtime~Greeter1到4。这也符合预期,使用哪一个入口的代码,就也加载它对应的runtime文件。

让咱们回到最初

如今,咱们仍是回到最初的一个简单的例子,结束咱们今天的研究。

基本场景

不考虑异步加载模块,只是分离业务代码,第三方库代码和runtime代码。

配置

入口文件index.js,里面只引用了react。 配置以下:

module.exports = {
  entry: {
    index: __dirname + "/app/index.js",
  },
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "[name].js", //打包后输出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  optimization: {
    runtimeChunk: true,
    splitChunks: {
      chunks: "initial", 
      automaticNameDelimiter: '~',  
      name: true,
      cacheGroups: { // 缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
        },
      }
    }
  },
}
复制代码

打包结果

image

咱们看到react是第三方库,提取到了vendors~index.js中,runtime代码,提取到了runtime-index.js,业务代码,就是index.js。

结束语

Webpack的官方文档,没有解释的那么清楚,对于Webpack的学习,须要多多动手,在实践中,帮助咱们学习体会Webpack。

SplitChunksPlugin要理解起来仍是稍微复杂一点的,它的设计就是为了搞定复杂的拆分状况。但摸清它的原理后,发现它仍是很强大的,经过几项配置,就能够完成复杂状况下的代码拆分。

相关文章
相关标签/搜索