Skip to content

自动分包

基本原理

不同与手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制

因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要

要控制自动分包,关键是要配置一个合理的分包策略

有了分包策略之后,不需要额外安装任何插件,webpack 会自动的按照策略进行分包

实际上,webpack 在内部是使用SplitChunksPlugin进行分包的 过去有一个库CommonsChunkPlugin也可以实现分包,不过由于该库某些地方并不完善,到了webpack4之后,已被SplitChunksPlugin取代

分包简单流程

从分包流程中至少可以看出以下几点:

  • 分包策略至关重要,它决定了如何分包
  • 分包时,webpack 开启了一个新的 chunk,对分离的模块进行打包
  • 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新 chunk 的产物

分包策略的基本配置

webpack 提供了optimization配置项,用于配置一些优化信息

其中splitChunks是分包策略的配置

js
module.exports = {
  optimization: {
    splitChunks: {
      // 分包策略
    },
  },
};

事实上,分包策略有其默认的配置,我们只需要轻微的改动,即可应对大部分分包场景

  1. chunks

该配置项用于配置需要应用分包策略的 chunk

我们知道,分包是从已有的 chunk 中分离出新的 chunk,那么哪些 chunk 需要分离呢

chunks 有三个取值,分别是:

  • all: 对于所有的 chunk 都要应用分包策略
  • async:【默认】仅针对异步 chunk 应用分包策略
  • initial:仅针对普通 chunk 应用分包策略

所以,你只需要配置chunksall即可

  1. maxSize

该配置可以控制包的最大字节数

如果某个包(包括分出来的包)超过了该值,则 webpack 会尽可能的将其分离成多个包

但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积

另外,该配置看上去很美妙,实际意义其实不大

因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存

虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化

如果要进一步减少公共模块的体积,只能是压缩和tree shaking

分包策略的其他配置

如果不想使用其他配置的默认值,可以手动进行配置:

  • automaticNameDelimiter:新 chunk 名称的分隔符,默认值~
  • minChunks:一个模块被多少个 chunk 使用时,才会进行分包,默认值 1
  • minSize:当分包达到多少字节后才允许被真正的拆分,默认值 30000

缓存组

之前配置的分包策略是全局的

而实际上,分包策略是基于缓存组的

每个缓存组提供一套独有的策略,webpack 按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包

默认情况下,webpack 提供了两个缓存组:

js
module.exports = {
  optimization: {
    splitChunks: {
      //全局配置
      cacheGroups: {
        // 属性名是缓存组名称,会影响到分包的chunk名
        // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
        },
        default: {
          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
          priority: -20, // 优先级
          reuseExistingChunk: true, // 重用已经被分离出去的chunk
        },
      },
    },
  },
};

很多时候,缓存组对于我们来说没什么意义,因为默认的缓存组就已经够用了

但是我们同样可以利用缓存组来完成一些事情,比如对公共样式的抽离

js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        styles: {
          test: /\.css$/, // 匹配样式模块
          minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
          minChunks: 2, // 覆盖默认的最小chunk引用数
        },
      },
    },
  },
  module: {
    rules: [
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      chunks: ["index"],
    }),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:5].css",
      // chunkFilename是配置来自于分割chunk的文件名
      chunkFilename: "common.[hash:5].css",
    }),
  ],
};

配合多页应用

虽然现在单页应用是主流,但免不了还是会遇到多页应用

由于在多页应用中需要为每个 html 页面指定需要的 chunk,这就造成了问题

js
new HtmlWebpackPlugin({
  template: "./public/index.html",
  chunks: ["index~other", "vendors~index~other", "index"],
});

我们必须手动的指定被分离出去的 chunk 名称,这不是一种好办法

幸好html-webpack-plugin的新版本中解决了这一问题

shell
npm i -D html-webpack-plugin@next

做出以下配置即可:

js
new HtmlWebpackPlugin({
  template: "./public/index.html",
  chunks: ["index"],
});

它会自动的找到被index分离出去的 chunk,并完成引用

目前这个版本仍处于测试解决,还未正式发布

原理

自动分包的原理其实并不复杂,主要经过以下步骤:

  1. 检查每个 chunk 编译的结果(在 webpack 的编译过程中,每一个 chunk 会被记录到一个资源列表中去。)
  2. 根据分包策略,从资源列表找到那些满足策略的模块
  3. 根据分包策略,生成新的 chunk 打包这些模块(代码有所变化)
  4. 把打包出去的模块从原始包中移除,并修正原始包代码
js
//main chunk
./src/index.js 						code
./node_modules/jquery/index.js		code
./node_modules/lodash/index.js		code
./src/common.css					code

//other chunk
./src/other.js 						code
./node_modules/jquery/index.js		code
./node_modules/lodash/index.js		code
./src/common.css					code

//根据分包策略,生成新的chunk并重新打包,将打包出去的模块从原始chunk中移除,并修正
//main chunk
./src/index.js 						code

//other chunk
./src/other.js 						code

//vendors~main~
./node_modules/jquery/index.js		code
./node_modules/lodash/index.js		code
./src/common.css					code

在代码层面,有以下变动

  1. 分包的代码中,加入一个全局变量,类型为数组,其中包含公共模块的代码
  2. 原始包的代码中,使用数组中的公共代码
js
//分包出来的结果中会有webpackJsonp,可以在控制台查看
window.webpackJsonp = window.webpackJsonp || [];

最佳实践

js
import $ from "jquery";
import _ from "lodash";
import common from "./common";
import "./common.css";
import "./index.css";

console.log("我是入口index");
js
import $ from "jquery";
import _ from "lodash";
import common from "./common";
import "./common.css";
import "./other.css";

console.log("我是other.js");
js
// 公共模块
export default "common module";
css
.index {
  color: #000;
}
css
body {
  background-color: #666;
}
css
.red {
  color: red;
}
js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  //只有在生产环境下才考虑传输性能,在开发环境下传输的服务器是在同一台机器上
  mode: "production",
  entry: {
    main: "./src/index.js",
    other: "./src/other.js",
  },
  output: {
    filename: "[name].[hash:5].js",
  },
  //loader
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  //plugins
  plugins: [
    new CleanWebpackPlugin(),
    //分离css文件
    new MiniCssExtractPlugin({
      filename: "[name].[hash:5].css",
      // 控制分包的文件名
      //   chunkFilename: "common.[hash:5].css",
    }),
    //
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      //新版本的html-webpack-plugin可以自动引入chunk的文件以及从中分包出去的文件到html中
      chunks: ["main"],
    }),
  ],
  //   分包策略
  optimization: {
    //1.全局的
    // splitChunks: {
    //   //指定需要分包的chunk,默认值是async
    //   chunks: "all",
    //   //超过60kb的模块会再进行切分,但是分包的基础单位是模块
    //   maxSize: 60000,
    //   //指定分包生成文件的连接符
    //   automaticNameDelimiter: "~",
    //   //一个模块被多少个[chunk!!!]使用时,才会进行分包,默认值1
    //   minChunks: 1,
    //   //当分包达到多少字节后才允许被真正的拆分,默认值30000
    //   //当希望给common.js分包出去,发现没起作用,就是因为common.js文件没达到这个配置项的默认值
    //   minSize: 0,
    // },

    //2.基于缓存组的
    splitChunks: {
      // 全局配置
      chunks: "all",
      cacheGroups: {
        //默认下,webpack提供了两个默认的缓存组,绝大多数默认的配置已经够用了
        // 属性名是缓存组名称,会影响到分包的chunk名
        // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
        },
        default: {
          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
          priority: -20, // 优先级
          // 重用已经被分离出去的chunk,已经分出去的包就不要再分包了,避免重新分离相同的公共模块
          reuseExistingChunk: true,
        },
        //某些情况下,我们可以利用缓存组来将公共模块分离出来
        styles: {
          test: /\.css$/,
          minChunks: 2,
          minSize: 0,
        },
      },
    },
  },

  stats: {
    colors: true,
    chunks: false,
    modules: false,
  },
};
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body></body>
</html>
json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "dev": "webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^3.4.2",
    "html-webpack-plugin": "^4.0.0-beta.11",
    "jquery": "^3.4.1",
    "lodash": "^4.17.15",
    "mini-css-extract-plugin": "^0.9.0",
    "webpack": "^4.41.6",
    "webpack-cli": "^3.3.11"
  }
}

生成的 dist 文件夹目录:

自动分包文件代码目录

MIT License