Webpack实战(八):教你搞懂webpack如果实现代码分片(code splitting)

4,346 阅读8分钟

2020年春节已过,本来打算回郑州,却因为新型冠状病毒感染肺炎的疫情公司推迟了上班的时间,我也推迟了去郑州的时间,在家多陪娃几天。以前都是在书房学习写博客,今天比较特殊,抱着电脑,在楼顶晒着太阳,陪着家人,写着博客。

前面的几篇文章主要告诉大家如何安装、配置webpack、webpack实现样式分离等,今天这篇文章主要跟大家分享如果webpack如何实现代码分片。

现在工程项目中,实现高性能应用的其中重要的一点就是让用户每次只加载必要的资源,优先级别不太高的资源采用延迟加载等技术渐进地进行加载获取。

Webpack 作为打包工具所特有的一项技术就是代码分片技术,通过这项技术我们可以把代码按照特定的形式进行拆分,使用按需加载资源,不必要全部加载下来。 代码分片可以有效降低首屏加载资源的大小,但是我们同时又面临着其他问题,比如如何对项目模块进行分片,分片后的资源如何进行管理等等。今天我们需要对这些问题进行分析解决。

利用入口划分代码

在Webpack中,配置参数中每个入口都将生成一个对应的资源文件,通过入口的配置我们可以进行一些简单有效的代码拆分。

对于项目中常常会引入一些第三方库和工具,这些一般不会改动的,可以把它们单独放在一个入口中,由该入口的资源不会经常更新,因此可以有效利用客户端缓存这些资源,让用户不必在每次请求页面时候都重新加载。

//webpack.config.js
entry: {
	index: './index.js',
	lib: ['lib-1', 'lib-2']
}

//index.html
<script src="dist/lib.js"></script>
<script src="dist/index.js"></script>

这种拆分方法主要适合于那些将接口绑定在全局对象上的库,因为业务代码中的模块无法直接引用库中的模块,二者属于不同的依赖树。

对于多页面应用来说,我们可以利用入口划分的方式拆分代码。比如,为每一个页面创建一个入口,并放入只涉及该页面的代码,同时再创建一个入口来包含所有公共模块,并使每个页面都进行加载。但是这样仍会带来公共模块与业务模块处于不同依赖树的问题。另外,很多时候不是所有的页面都需要这些公共模块。这就需要我们利用webpack专用的插件来解决这种问题了。

CommonsChunkPlugin

CommonsChunkPlugin是webpack4之前内部自带的插件,webpack4之后用的是SplitChunks。CommonsChunkPlugin主要是用来提取第三方库和公共模块,避免首屏加载的bundle文件或者按需加载的bundle文件体积过大,从而导致加载时间过长,是一把优化项目的利器。

优点:

  • 开发过程中减少了重复模块打包,可以提升开发速度;
  • 减小整体资源体积;
  • 合理分片后的代码可以更有效地利用客户端缓存。 首页通过一个简单的例子也说明,假设我们当前项目中有a.js 和b.js 两个入口文件,并且都引入了react,下面是未使用CommonsChunkPlugin的配置
//webpack.config.js
module.exports = {
	entry: {
		a: './a.js',
		b: './b.js'
	},
	output: {
		filename: '[name].js'
	}
}

//a.js
import React from 'React'
... //省略

// b.js 
import React from 'React'
... //省略

如果打包,从打包的资源体积可以看出,react被分别打包到a.js 和b.js中。

更改webpack.config.js,添加CommonsChunkPlugin配置

const webpack = require('webpack');
module.exports = {
	entry: {
		a: './a.js',
		b: './b.js'
	},
	output: {
		filename: '[name].js'
	},
	plugins: [
	new webpack.optimize.CommonsChunkPlugin({
	name: 'commons',
	filename: 'commons.js'
})
]
}

在配置文件的头部引入Webpack,接着使用其内部CommonsChunkPlugin函数创建了一个插件实例,并传入配置对象,配置参数可以理解为

  • name:用于指定公共chunk的名字
  • filename: 提取后的资源文件名 打包后可以看到,产出的资源中多了一个commons.js,而a.js 和b.js文件的体积也减少了,这是由于把react及依赖的模块提到commons.js的原因。 不过我们需要注意的是,我们需要在页面引入其他j s之前,先引入公用的commons.js文件。

在提取公共模块方面,CommonsChunkPlugin可以满足很多场景的需求,但是它也有一些欠缺的地方。 1)一个CommonsChunkPlugin只能提取一个vendor,假如我们想提取多个vendor则需要配置多个插件,这会增加很多重复的配置代码。

2)前面我们提到的manifest实际上会使浏览器多加载一个资源,这对于页面渲染速度是不友好的。

3)由于内部设计上的一些缺陷,CommonsChunkPlugin在提取公共模块的时候会破坏掉原有Chunk中模块的依赖关系,导致难以进行更多的优化。比如在异步Chunk的场景下CommonsChunkPlugin并不会按照我们的预期正常工作。

optimization.SplitChunks

optimization.SplitChunks(简称SplitChunks)是Webpack 4为了改进CommonsChunk-Plugin而重新设计和实现的代码分片特性。它不仅比CommonsChunkPlugin功能更加强大,还更简单易用。

配置文件web pack.config.js为:

const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
  context: path.join(__dirname, './src'),
  entry: {
    index: './index.js'
  },
  output: {
    // path: path.join(__dirname, 'dist'),
    filename: 'index.js',
    publicPath: '/dist/'
  },
  mode: 'development',
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', {
          loader: 'css-loader',
          options: {
            modules: {
              localIdentName: '[path][name]__[local]--[hash:base64:5]',
            }
          }
        }]
      },
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            presets: [
              [
                'env', {
                  modules: false
                }
              ]
            ]
          }
        }
      }
    ],
  }
}

需要引入的两个index.js文件和index2.js文件

// index
import index2 from  './index2.js';
import React from 'react'
document.write('index.js', React.version);

//index2
import React from 'react'
document.write('index2.js', React.version);


使用optimization.splitChunks替代了CommonsChunkPlugin,并指定了chunks的值为all,这个配置项的含义是,SplitChunks将会对所有的chunks生效(默认情况下,SplitChunks只对异步chunks生效,并且不需要配置) 打包结果如下图:

在这里插入图片描述

原本我们打包的结果应该是index.js,但是由于SplitChunks的存在,又生成了一个vendors~index.index.js,并且把react提取到了里面。 运行的效果如下图:

在这里插入图片描述
在使用CommonsChunkPlugin的时候,我们大多数时候是通过配置项将特定入口中的特定模块提取出来,也就是更贴近命令式的方式。而SplitChunks的不同之处在于我们只需要设置一些提取条件,如提取的模式、提取模块的体积等,当某些模块达到这些条件后就会自动被提取出来。SplitChunks的使用更像是声明式的。

SplitChunks默认情形下的提取条件:

  • 提取后的chunk可被共享或者来自node_modules目录。这一条很容易理解,被多次引用或处于node_modules中的模块更倾向于是通用模块,比较适合被提取出来。
  • 提取后的Javascript chunk体积大于30kB(压缩和gzip之前),CSS chunk体积大于50kB。这个也比较容易理解,如果提取后的资源体积太小,那么带来的优化效果也比较一般。
  • 在按需加载过程中,并行请求的资源最大值小于等于5。按需加载指的是,通过动态插入script标签的方式加载脚本。我们一般不希望同时加载过多的资源,因为每一个请求都要花费建立链接和释放链接的成本,因此提取的规则只在并行请求不多的时候生效。
  • 在首次加载时,并行请求的资源数最大值小于等于3。和上一条类似,只不过在页面首次加载时往往对性能的要求更高,因此这里的默认阈值也更低。

SplitChunks提取方式

SplitChunks 默认的提取方式是异步提取,当我们在chunks上配置参数为all的时候,不是异步资源也可以提取。

SplitChunks 配置

optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      minRemainingSize: 0,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 6,
      maxInitialRequests: 4,
      automaticNameDelimiter: '~',
      name: true,
      automaticNameMaxLength: 30,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }

(1)匹配模式通过chunks我们可以配置SplitChunks的工作模式。它有3个可选值,分别为async(默认)、initial和all。async即只提取异步chunk,initial则只对入口chunk生效(如果配置了initial则上面异步的例子将失效),all则是两种模式同时开启。

(2)匹配条件minSize、minChunks、maxAsyncRequests、maxInitialRequests都属于匹配条件,

(3)命名配置项name默认为true,它意味着SplitChunks可以根据cacheGroups和作用范围自动为新生成的chunk命名,并以automaticNameDelimiter分隔。如vendors~a~b~c.js意思是cacheGroups为vendors,并且该chunk是由a、b、c三个入口chunk所产生的。

(4)cacheGroups可以理解成分离chunks时的规则。默认情况下有两种规则——defaultVendors和default。defaultVendors用于提取所有node_modules中符合条件的模块,default则作用于被多次引用的模块。我们可以对这些规则进行增加或者修改,如果想要禁用某种规则,也可以直接将其置为false。当一个模块同时符合多个cacheGroups时,则根据其中的priority配置项确定优先级。

总结

有关webpack实现代码分片的几种方法:合理地规划入口,使用Commons-ChunkPlugin或SplitChunks就暂时分享到这里,这仅代表个人的观点,如想了解更多请扫描二维码

在这里插入图片描述
最后友情提醒大家要戴口罩,勤洗手,尽量减少外出,做好预防 措施,远离新型冠状病毒。