Webpack之SplitChunks插件用法详解

Webpack之SplitChunks插件用法详解

前言

SplitChunks插件是什么呢,简单的来说就是Webpack中一个提取或分离代码的插件,主要作用是提取公共代码,防止代码被重复打包,拆分过大的js文件,合并零散的js文件。

提到前端优化,提取公共代码是必不可少的手段。在Webpack出现前,提取公共代码是人为去处理,而SplitChunks插件的作用就是通过配置让Webpack去帮你提取公共代码。Webpack创始人的初衷也是希望能有更多时间写更多代码,所以这种纯体力的劳动交给Webpack去完成。

所以SplitChunks插件是前端进阶的一项必备技能,下面就详细介绍SplitChunks插件的用法。

二、工欲利其事必先利其器

因为SplitChunks插件会提取模块并打包生成 js 文件。先学会给打包生成的 js 文件命名,不然不好确认打包生成的 js 文件是不是你想要的。Webpack中给打包生成的 js 文件命名有以下几种方法。

1、output.filename

此选项给打包后的入口 js 文件命名,下图中的 app.js 文件就是打包后的入口js文件。

  • 在单入口的Vue项目中,打包前的入口文件,在 vue.config.js 中配置module.exports = { configureWebpack:{ entry:'./src/main.js', } } 复制代码
    打包后的入口文件,在 vue.config.js 中配置module.exports = { configureWebpack:{ output:{ filename: 'js/appCS.js' }, } } 复制代码
    为什么前面要加个 js/ ,因为该 js 文件是 output:path 选项指定的目录下生成的,其默认为根目录 / 。打包结果如下所示


  • 在多入口的Vue项目中,打包前的入口文件,在 vue.config.js 中配置module.exports = { configureWebpack:{ entry: { appCS: './src/main.js' }, } } 复制代码
    打包后的入口文件,在 vue.config.js 中配置module.exports = { configureWebpack:{ output:{ filename: 'js/[name].js' }, } } 复制代码
    [name] 是入口文件模块名称。打包结果如下所示

会发现多个 app.js ,这是因为configureWebpack去采用合并策略来配置Webpack。要去掉 app.js 要用chainWebpack来配置。配置如下module.exports = { chainWebpack: config =>{ config.entryPoints.delete('app').end().entry('appCS').add('./src/main.js') } } 复制代码
打包结果如下所示


2、output.chunkFilename

此选项给打包后的非入口 js 文件命名,那下图红框中所示就是非入口 js 文件

在vue.config.js中配置

module.exports = {
    configureWebpack:{
        output:{
            chunkFilename: 'CS.[name].js'
复制代码

打包结果如下所示

然而 output.chunkFilename 并不能改变 chunk-0a4e15c9 这样名字字段。

3、webpackChunkName

webpackChunkName :块的名称, [request] 可解释为实际的解析文件名。可在路由懒加载中使用

function load(component) {
    return () => import(/* webpackChunkName: "[request]" */ `views/${component}`)
const routes = [
            path: '/apiApply',
            name: 'apiApply',
            component: load('api_manage/api_apply'),
复制代码

打包结果如下所示

src/views/api_manage/api_apply/index.vue 这个组件打包后生成的js文件是 api_manage-api_apply.48227bf7.js

如果不用 [request] 也可以这么写。

component: () =>import(/* webpackChunkName: "api_manage-api_apply"*/ 'src/views/api_manage/api_apply'),
复制代码

再看一下其它 js 文件,发现还有 chunk-xxx.js 类的文件,如下图红框中所示

这些 js 文件的命名要在SplitChunks插件中设置了。

二 、SplitChunks插件配置选项

  • chunks 选项,决定要提取那些模块。
    • 默认是 async :只提取异步加载的模块出来打包到一个文件中。
      • 异步加载的模块:通过 import('xxx') require(['xxx'],() =>{}) 加载的模块。


    • initial :提取同步加载和异步加载模块,如果xxx在项目中异步加载了,也同步加载了,那么xxx这个模块会被提取两次,分别打包到不同的文件中。
      • 同步加载的模块:通过 import xxx require('xxx') 加载的模块。


    • all :不管异步加载还是同步加载的模块都提取出来,打包到一个文件中。


  • minSize 选项:规定被提取的模块在压缩前的大小最小值,单位为字节,默认为30000,只有超过了30000字节才会被提取。
  • maxSize 选项:把提取出来的模块打包生成的文件大小不能超过maxSize值,如果超过了,要对其进行分割并打包生成新的文件。单位为字节,默认为0,表示不限制大小。
  • minChunks 选项:表示要被提取的模块最小被引用次数,引用次数超过或等于minChunks值,才能被提取。
  • maxAsyncRequests 选项:最大的按需(异步)加载次数,默认为 6。
  • maxInitialRequests 选项:打包后的入口文件加载时,还能同时加载js文件的数量(包括入口文件),默认为4。
  • 先说一下优先级 maxInitialRequests / maxAsyncRequests < maxSize < minSize
  • automaticNameDelimiter 选项:打包生成的js文件名的分割符,默认为 ~
  • name 选项:打包生成js文件的名称。
  • cacheGroups 选项,核心重点, 配置提取模块的方案 。里面每一项代表一个提取模块的方案。下面是 cacheGroups 每项中特有的选项,其余选项和外面一致,若 cacheGroups 每项中有,就按配置的,没有就使用外面配置的。
    • test 选项:用来匹配要提取的模块的资源路径或名称。值是正则或函数。
    • priority 选项:方案的优先级,值越大表示提取模块时优先采用此方案。默认值为0。
    • reuseExistingChunk 选项: true / false 。为 true 时,如果当前要提取的模块,在已经在打包生成的 js 文件中存在,则将重用该模块,而不是把当前要提取的模块打包生成新的 js 文件。
    • enforce 选项: true / false 。为 true 时,忽略 minSize minChunks maxAsyncRequests maxInitialRequests 外面选项


配置选项很多,下面在实际项目中使用SplitChunks,让你更深刻理解这些配置选项。

首先了解一下SplitChunks在Vue Cli3中的默认配置 。在Vue Cli3源码中这样配置

整理后默认配置如下所示:

module.exports = {
    configureWebpack:config =>{
        return {
            optimization: {
                splitChunks: {
                    chunks: 'async',
                    minSize: 30000,
                    maxSize: 0,
                    minChunks: 1,
                    maxAsyncRequests: 6,
                    maxInitialRequests: 4,
                    automaticNameDelimiter: '~',
                    cacheGroups: {
                        vendors: {
                            name: `chunk-vendors`,
                            test: /[\\/]node_modules[\\/]/,
                            priority: -10,
                            chunks: 'initial'
                        common: {
                            name: `chunk-common`,
                            minChunks: 2,
                            priority: -20,
                            chunks: 'initial',
                            reuseExistingChunk: true
复制代码

先安装个webpack-bundle-analyzer插件,可以可视化分析打包后的文件。

npm install webpack-bundle-analyzer --save-dev
复制代码

vue.config.js 中引入插件

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports={
    configureWebpack:config =>{
        return {
            plugins:[
                new BundleAnalyzerPlugin()
复制代码

打包后,会在浏览器自动打开 127.0.0.1:8888/ ,内容如下所示

会发现里面有个 chunk-vendors.js 文件。是 cacheGroups 中vendors这个方案打包出来的js文件。

可以用 name 选项来修改 chunk-vendors.js 文件的名字,,代码如下

vendors: {
    name: `app-chunk-vendors`,
    test: /[\\/]node_modules[\\/]/,
    priority: -10,
    chunks: 'initial'
复制代码

打包后,会发现 chunk-vendors.js 文件已经变成了 app-chunk-vendors.js 文件,里面内容不变。

三、SplitChunks实战操作之入口文件

先去掉 cacheGroups 里面的方案,再打包一下。

cacheGroups: {
    vendors: false,
    common: false
复制代码

app.a502ce9a.js chunk-be34ce9a.ceff3b64.js 这两个js文件是由项目中 main.js 这个入口文件打包生成的。

例如 app.js 文件中有element-ui、moment、jquery、vue、router、store、jsencrypt等内容。这些都是在 main.js 中引入

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import JsEncrypt from 'jsencrypt';
import $ from 'jquery';
import ElementUI from 'element-ui';
Vue.use(ElementUI);
import treeSelect from 'fxft-tree-select';
Vue.use(treeSelect);
import moment from 'moment';
Vue.prototype.moment = moment;
//注册全局变量、全局函数
import base from 'service/base';
Vue.use(base);
//注册打印插件
import print from  'service/print';
Vue.use(print);
const vm = new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app')
window.vm = vm;
复制代码

打包生成的 index.html 中有段代码是这么写。

<div>
    <div id=app></div>
    <script src=/js/app.a502ce9a.js></script>
复制代码

表明 app.js 是项目一开始就要加载的,会影响首屏的加载时间。那么可以在 main.js 中去掉一些在首屏中暂时用不到的引入,比如这些都可以暂时去掉。

import JsEncrypt from 'jsencrypt';
import treeSelect from 'fxft-tree-select';
Vue.use(treeSelect);
//注册打印插件
import print from  'service/print';
Vue.use(print);
复制代码

打包后再看分析图,会发现jsencrypt等内容都在 app.js 中都消失了。

为什么说 chunk-be34ce9a.js 也是从 main.js 打包生成的?因为在 main.js 中有段代码:

//注册全局变量、全局函数
import base from 'service/base';
Vue.use(base);
复制代码

在看 service/base.js 文件中有

import('./Export2Excel').then(res => {
    res.export_json_to_excel(defaultOpition);
复制代码

Export2Excel.js 是异步加载,在 service/Export2Excel.js

import { saveAs } from 'file-saver'
import XLSX from 'xlsx'
复制代码

相当,file-saver、xlsx也是异步加载,所以file-saver、xlsx会被提取出来打包生成 chunk-be34ce9a.js 文件。

在默认配置下,main.js中异步加载或间接异步加载的模块,都会被另外打包生成一个js文件。

如果要把从 node_modules 中加载的模块全部打包到一个js文件中,要怎么做呢?Vue Cli3中的已经帮我们做了。

cacheGroups: {
    vendors: {
        name: `chunk-vendors`,
        test: /[\\/]node_modules[\\/]/,
        priority: -10,
        chunks: 'initial'
复制代码

其核心是 test 选项,匹配项目从 node_modules 中加载的模块并提取打包生成 chunk-vendors.js 文件。 打包后再从分析图中搜索 node_modules ,发现还是由很多文件中含有从 node_modules 中加载的模块,和预期的不一样。

这是 chunks 选项在作怪,值 initial 表示如果xxx在项目中异步加载或同步加载多少次,那么xxx这个模块也会被提取多少次,分别打包到不同的文件中。core-js库在项目中每个文件都会加载到,故它会提取多次。

只要把 chunks 选项的值改成 all (不管异步加载还是同步加载的模块都是提取打包到一个文件中),就可以把从 node_modules 中加载的模块全部打包到一个js文件中。

发现chunk-vendors.js的大小有点大了,有1.91MB,还是项目初始化时需要加载的js文件,大小过大会导致首屏加载时间过长。要优化一下,由两种方法

第一种用externals来优化,看我的另一篇文章Webpack之externals用法详解。

第二种用SplitChunks优化。例如要把 element chunk-vendors.js 提取出来,要在 cacheGroups 中配置:

element: {
    chunks: 'all',
    name: `element-ui`,
    test: /[\\/]element-ui[\\/]/,
    priority: 0,
复制代码

其中要注意 priority 选项,要把element单独提取出来, priority 的值必须比vendors方案中的 priority 的值大 ,不然提取不出来。

打包后可看到element被打包生成新的 element-ui.js 文件,chunk-vendors.js大小变成1.27MB,比原来的1.91MB有减小。此外可以自己提取xlsx、moment、jquery等第三方依赖。

四、SplitChunks实战操作之非入口文件

在分析图中,除了入口文件,还有很多js文件,这些文件中有一大部分是项目中组件打包生成的。

如果在实现路由懒加载时,用到 /*webpackChunkName:"[request]"*/ ,那么在由组件打包生成的 js 文件名上,可以得知这个 js 文件是哪个组件打包生成的。

图中的 base_info_manage-group_info_set-ability_bind_set.85b419a1.js 是项目中 views/base_info_manage/group_info_set/bility_bind_set/index.vue 这个组件打包生成的。

base_info_manage-group_info_set-ability_bind_set-edit.08f91768.js 是项目中 views/base_info_manage/group_info_set/bility_bind_set/edit.vue 这个组件打包生成的。

另外会发现 chunk-5c1416e3.1cbcb0ec.js 的内容怎么跟 base_info_manage-group_info_set-ability_bind_set-edit.08f91768.js 的内容相似,只少了src/api的内容。而且 api/common.js api/ability_bind_set.js edit.vue mixins 等模块被重复打包了好几次。

可以用SplitChunks提取一下这些模块。避免重复打包,减少打包生成的文件总体大小。

cacheGroups 中配置

api: {
    name: 'api',
    test: /[\\/]api[\\/]/,
    priority: 0,
复制代码

当提取多个模块打包生成文件时, name 选项为必填。

打包后再看分析图会发现 api/common.js api/ability_bind_set.js 已经被提取到 api.05ad5193.js 中了

接着提取mixins模块,在 cacheGroups 中配置

mixins: {
    name: 'mixins',
    test: /[\\/]mixins[\\/]/,
    priority: 0,
复制代码

打包后再看分析图发现mixins模块已经被提取到 mixins.8d1d6f50.js 中。

接着提取 edit.vue 模块,在 cacheGroups 中配置

base_info_manage: {
    name: 'base_info_manage',
    test: /[\\/]base_info_manage[\\/]/,
    minChunks: 2,
    priority: 0,
复制代码

其中 minChunks 选项,必须为2,因为从上面分析图来看,edit.vue被引用了2次,而index.vue只被引用了1次。如果为1则index.vue也会被提取出来。如果为2以上的值则edit.vue不会被提取出来。

打包后再看分析图发现 edit.vue 模块已经被提取到 base_info_manage.d5c14c01.js 中。 如果觉得 base_info_manage.d5c14c01.js 文件太大,有两种方法可以处理。

第一种是利用 maxSize 选项,提取模块后打包生成的文件大小不能超过maxSize值,如果超过了,要再提取并打包生成新的文件。

base_info_manage: {
    name: 'base_info_manage',
    test: /[\\/]base_info_manage[\\/]/,
    minChunks: 2,
    priority: 0,
    maxSize: 102400
复制代码

打包后再看分析图会发现 base_info_manage.js 已经被分割成五个小js文件了。

第二种是按 base_info_manage 文件夹下的子文件夹来继续提取,例如 base_info_manage 文件夹下有个子文件叫 group_info_set 。在 cacheGroups 中配置

group_info_set: {
    name: 'group_info_set',
    test: /[\\/]base_info_manage[\\/]group_info_set[\\/]/,
    minChunks: 2,
    priority: 10,