Skip to content

分包

分包就是将一个整体的代码,分布到不同的打包文件中。

分包的作用是减少公共代码,降低打包文件的总体积,尤其是项目存在一些大型的第三方库时。

当多个 chunk 引入了公共模块或是公共模块体积较大、变动较少时,就可以进行分包。

分包主要分为手动分包和自动分包。

手动分包

基本原理

手动的分包的总体思路是:

  1. 先单独打包公共模块

image-20220716133727730

公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单。

  1. 根据入口模块进行正常打包

打包时,如果发现模块中使用了资源清单中描述的模块,则不会再次打包对应的模块。

js
// 源码,入口文件index.js
// 这里不会进行依赖分析了
import $ from 'jquery'
import _ from 'lodash'
_.isArray($('.red'));

由于资源清单中包含jquerylodash两个模块,因此打包结果的大致格式是:

js
(function(modules){
  //...
})({
  // index.js文件的打包结果并没有变化
  "./src/index.js":
  function(module, exports, __webpack_require__){
    var $ = __webpack_require__("./node_modules/jquery/index.js")
    var _ = __webpack_require__("./node_modules/lodash/index.js")
    _.isArray($(".red"));
  },
  // 由于资源清单中存在,jquery的所有代码并不会出现在这里
  "./node_modules/jquery/index.js":
  function(module, exports, __webpack_require__){
    // 代码内容变为,导出全局变量jquery
    module.exports = jquery;
  },
  // 由于资源清单中存在,lodash的所有代码并不会出现在这里
  "./node_modules/lodash/index.js":
  function(module, exports, __webpack_require__){
    // 代码内容变为,导出全局变量lodash
    module.exports = lodash;
  }
})

打包公共模块

打包公共模块是一个独立的打包过程。

  1. 单独打包公共模块,暴露变量名
js
// webpack.dll.config.js
module.exports = {
  mode: 'production',
  entry: {
    jquery: ['jquery'],
    lodash: ['lodash']
  },
  output: {
    filename: 'dll/[name].js',
    library: '[name]'
  }
}
  1. 利用webpack自带的DllPlugin插件生成资源清单
js
// webpack.dll.config.js
var webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DllPlugin({
      path: path.resolve(__dirname, './dist/dll', '[name].manifest.json'), //资源清单的保存位置
      name: '[name]' //资源清单中,暴露的变量名,跟output.library保持一致
    })
  ]
}

提示

资源清单可以放在任意目录中,因为资源清单并不参与最终的代码运行,它只在打包我们自己的代码时运行。

执行打包命令:webpack --config webpack.dll.config.js,即可完成分包。

使用公共模块

  1. 在页面中手动引入公共模块
html
<script src="./dll/jquery.js"></script>
<script src="./dll/lodash.js"></script>
  1. 重新设置clean-webpack-plugin

如果使用了插件clean-webpack-plugin,为了避免它把公共模块清除,需要做出以下配置:

js
new CleanWebpackPlugin({
  // 要清除的文件或目录
  // 排除掉dll目录本身和它里面的文件
  cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
})

目录和文件的匹配规则使用的是 globbing patterns

  1. 使用DllReferencePlugin控制打包结果
js
module.exports = {
  plugins:[
    new webpack.DllReferencePlugin({
      manifest: require('./dll/jquery.manifest.json')
    }),
    new webpack.DllReferencePlugin({
      manifest: require('./dll/lodash.manifest.json')
    })
  ]
}

总结

手动打包的过程

  1. 开启output.library暴露公共模块
  2. DllPlugin创建资源清单
  3. DllReferencePlugin使用资源清单

手动打包的注意事项

  1. 资源清单不参与运行,可以不放到打包目录中
  2. 记得手动引入公共JS,以及避免被删除
  3. 不要对小型的公共JS库使用

优点

  1. 极大提升自身模块的打包速度
  2. 极大的缩小了自身文件体积
  3. 有利于浏览器缓存第三方库的公共代码

缺点

  1. 使用非常繁琐,需要手动引入模块,有新模块时,还需要手动更新配置
  2. 使用场景比较有限,如果第三方库中包含重复代码,则效果不太理想

自动分包

基本原理

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

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

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

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

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

image-20220717113541137

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

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

分包策略的基本配置

分包策略通过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
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

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解决了这一问题,只需做出以下配置即可:

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

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

总结

手动打包的过程

  1. 检查每个chunk编译的结果
  2. 根据分包策略,找到那些满足策略的模块
  3. 根据分包策略,生成新的chunk打包这些模块(代码有所变化)
  4. 把打包出去的模块从原始包中移除,并修正原始包代码

代码会有一下变动:

  1. 分包的代码中,加入一个全局变量,类型为数组,其中包含公共模块的代码
  2. 原始包的代码中,使用数组中的公共代码