webpack4之splitChunks

1,964 阅读3分钟

1、什么是chunk、module、bundle

  • module:webpack支持commonJS、es6模块化规范,module就是通过import引入的代码
  • chunk:chunk包含着module,是一对多或一对一。chunk是webpack根据功能拆分出来的,分为三种情况:
    1. 项目入口(entry)
    2. 通过import()动态引入的代码
    3. 通过splitChunks拆分代码
  • bundle: bundle是webpack打包后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出

2、splitChunks的默认配置

splitChunks就算你什么配置都不做它也是生效的,源于webpack有一个默认配置,这也符合webpack4的开箱即用的特性,它的默认配置如下:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};
//entry1.js
'use strict';
import React from 'react'
import ReactDOM from 'react-dom'

const App = () => {
    let Page1 = null

    import(/* webpackChunkName: "page1" */'./page1').then(comp => {
        Page1 = comp
    });
    return (
        <div>
            <div>App</div>
            {Page1 && <Page1/>}
        </div>
    )
}
document.addEventListener('DOMContentLoaded', () => {
    var root = document.createElement('div');
    document.body.appendChild(root);

    ReactDOM.render(<App />, root);
});
//page1.js
'use strict';
import React from 'react'
import _ from 'lodash'

const Page1 = () => {
    console.log($)

    return (
        <div>
            <div>Page1</div>
        </div>
    )
}

export default Page1
//webpack.config.js
module.exports = {
    mode: 'development',
    resolve: {
        extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.scss'],
    },
    entry: {
        entry1: './src/entry1.js',
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].js',
        chunkFilename: '[name].chunk.js', //动态加载模块名称
        publicPath: path.join(__dirname, 'dist/'), //动态加载模块路径
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './template.html',
            filename: 'index.html',
        }),
    ]
}

打包后结果分析:

  1. entry1.js就是打包之后的入口文件,webpack会把入口文件单独拆成一个chunk
  2. page1.chunk.js,动态加载的文件webpack会将其拆分为一个chunk
  3. vendors~page1.chunk.js,这个是对page1文件里面引入的第三方库进行打包,具体就是lodash那个第三方库了,这个涉及到cacheGroup稍后说明

但是我们发现有一个文件没有拆分出来,那就是entry1里面引入的第三方库react-dom,这个是为什么呢,这个就要涉及到我们接下来讲到的chunks属性的配置

3、splitChunks.chunks属性

chunks可以有三种取值:

  1. async表示从异步加载模块(动态加载import())中拆分
  2. initial表示入口模块拆分
  3. all表示包含以上两种

chunks的默认配置是async,也就是只从动态加载得模块里面进行拆分。上述能够把page1.js引入的第三方模块拆分出来,但是因为entry1.js属于入口chunk所以它引入的第三方库react-dom就没能拆分出。

修改chunks: all,入口模块entry.js的第三方依赖已经被成功拆分出来了

chunks: initial,page1.js的第三方依赖没有被拆分出来,入口模块entry.js的第三方依赖被拆分出来

4、splitChunks.cacheGroups属性

cacheGroups其实是splitChunks里面最核心的配置,splitChunks默认有两个缓存组:vender和default。

上述能拆分,cacheGroups里面定义了vendors这个缓存组,它的test设置为 /[\\/]node_modules[\\/]/ 表示只筛选从node_modules文件夹下引入的模块,所以所有第三方模块才会被拆分出来。除此之外还有一个default缓存组,它会将至少有两个chunk引入的模块进行拆分,它的权重小于vendors。

//entry1.js和entry2.js
import React from 'react';
import ReactDOM from 'react-dom';
import Page1 from './page1';
import $ from './lib/jquery';

const App = () => {
    return (
        <div>
            <div>entry</div>
            <Page1/>
        </div>
    )
}
document.addEventListener('DOMContentLoaded', () => {
    var root = document.createElement('div');
            document.body.appendChild(root);
        
    ReactDOM.render(<App />, root);
});
//webpack.config.js
module.exports = {
    //...
    entry: {
        entry1: './src/entry1.js',
        entry2: './src/entry2.js',
    },
    optimization: {
        splitChunks: {
            chunks: 'all',
        }
    },
}

cacheGroups之外设置的约束条件比如说默认配置里面的chunks、minSize、minChunks等等都会作用于cacheGroups,除了test, priority 、 reuseExistingChunk,这三个是只能定义在cacheGroup这一层的。上一个例子中,为什么entry里面引入的第三方库react-dom只被entry1.js引入了一次就会被打包出来,因为默认的minChunks=1。这个属性会作用于所有的cacheGroups,但是cacheGroups也可以将上面的所有属性都重新定义,就会覆盖外面的默认属性,比如default这个缓存组就设置了minChunks=2,他会覆盖掉默认值1。

思考1:为什么entry1.js和entry2.js里面都引入了react-dom这个第三方库,它完全满足default这个cacheGroup的条件但是为什么没有被包含在entry1~entry2这个chunk中而是被纳入了vendor~entry1~entry2这个chunk里面了呢?

其实这是因为priority这个属性起了作用,它的含义是权重,如果有一个模块满足了多个缓存组的条件就会去按照权重划分,谁的权重高就优先按照谁的规则处理,default的priority是-20明显小于vendors的-10,所以会优先按照vendors这个缓存组拆分。

思考2:为什么entry1.js和entry2.js里面都引入了jquery(本地的)被打包成entry1~entry2这个chunk,而Page1没有打包?

因为Page1大小没有满足,minSize为30000的限制,而jquery满足了

5、splitChunks.maxInitialRequests属性

maxInitialRequests,它表示允许入口(entry)并行加载的最大请求数,之所以有这个配置也是为了对拆分数量进行限制,不至于拆分出太多模块导致请求数量过多而得不偿失。

注意以下几点

  • 入口文件本身算一个请求
  • 如果入口里面有动态加载得模块这个不算在内
  • 通过runtimeChunk拆分出的runtime不算在内
  • 只算js文件的请求,css不算在内
  • 如果同时又两个模块满足cacheGroup的规则要进行拆分,但是maxInitialRequests的值只能允许再拆分一个模块,那尺寸更大的模块会被拆分出来

默认是值是3

//entry1.js
'use strict';
import React from 'react'
import ReactDOM from 'react-dom'
import $ from './lib/jquery';
import OrgChart from './lib/orgchart'

const App = () => {
    return (
        <div>
            <div>entry2</div>
        </div>
    )
}
document.addEventListener('DOMContentLoaded', () => {
    var root = document.createElement('div');
    document.body.appendChild(root);

    ReactDOM.render(<App />, root);
});

//entry2.js
import React from 'react';
import ReactDOM from 'react-dom';
import $ from './lib/jquery';

const App = () => {
    return (
        <div>
            <div>entry2</div>
        </div>
    )
}
document.addEventListener('DOMContentLoaded', () => {
    var root = document.createElement('div');
            document.body.appendChild(root);
        
    ReactDOM.render(<App />, root);
});
//entry3.js
import React from 'react'
import ReactDOM from 'react-dom'
import OrgChart from './lib/orgchart';

const App = () => {
    return (
        <div>
            <div>App</div>
        </div>
    )
}
document.addEventListener('DOMContentLoaded', () => {
    var root = document.createElement('div');
    document.body.appendChild(root);

    ReactDOM.render(<App />, root);
});
//webpack.config.js
module.exports = {
    //...
    entry: {
        entry1: './src/entry1.js',
        entry2: './src/entry2.js',
        entry3: './src/entry2.js',
    },
    optimization: {
        splitChunks: {
            chunks: 'all',
            maxInitialRequests: 3,
        }
    },
}

entry1~entry2.chunk.js是entry1.js与entry2.js共同引入jquery大的包,entry1.js与entry3.js共同引入orgchart没有被拆分原因是什么?

我们看下entry1的并发请求数目前有哪些:

  1. entry1.js本身是一个对应的就是entry1.js这个文件
  2. vendors~entry1~entry2~entry3.chunk.js
  3. entry1~entry2.chunk.js

所以目前已经达到了最大的请求数3,并且jquery比orgchar大,这就是为什么不会吧orgchart.js再拆分出来的原因,那么如果我把maxInitialRequests改为4呢?

orgchart.js就被拆分出entry1~entry3.chunk.js这个chunk了

5、splitChunks.maxAsyncRequests属性

maxAsyncRequests是用来限制异步模块内部的并行最大请求数的,可以理解为是每个import()它里面的最大并行请求数量

注意以下

  1. import()文件本身算一个请求
  2. 并不算js以外的公共资源请求比如css
  3. 如果同时有两个模块满足cacheGroup的规则要进行拆分,但是maxInitialRequests的值只能允许再拆分一个模块,那尺寸更大的模块会被拆分出来

默认值是5 在上面例子中,修改entry1.js,增加page1.js

//entry1.js
'use strict';
import React from 'react'
import ReactDOM from 'react-dom'

const App = () => {
    let Page1 = null

    import(/* webpackChunkName: "page1" */'./page1').then(comp => {
        Page1 = comp
    });
    return (
        <div>
            <div>App</div>
            {Page1 && <Page1 />}
        </div>
    )
}
document.addEventListener('DOMContentLoaded', () => {
    var root = document.createElement('div');
    document.body.appendChild(root);

    ReactDOM.render(<App />, root);
});

//page1.js
'use strict';

import React from 'react'
import _ from 'lodash'
import $ from './lib/jquery';
import OrgChart from './lib/orgchart';

const Page1 = () => {
    return (
        <div>
            <div>Page1</div>
        </div>
    )
}

export default Page1;
//webpack.config.js
module.exports = {
    //...
    entry: {
        entry1: './src/entry1.js',
        entry2: './src/entry2.js',
        entry3: './src/entry2.js',
    },
    optimization: {
        splitChunks: {
            chunks: 'all',
            maxAsyncRequests: 3,
        }
    },
}

主要是看和page1有关的,因为page1是通过import()动态引入的

  1. vendors~page1.chunk.js是page1里面引入的第三方库lodash,这个是根据cacheGroups进行拆分的
  2. page1.chunk.js是page1.js文件本身
  3. entry2~page1.js这个拆分的entry2和page1的共用文件jquery.js,这个是根据cacheGroups进行拆分的

那么page1这个异步模块的并发请求数正好是设置的最大值3,符合maxAsyncRequests。

这里我们发现除了jquery.js之外page1.js和entry3.js还共同引入了orgchart.js文件 ,但是却没有被拆分出来,这就是因为maxAsyncRequests的限制,如果我们把值改为4呢?

page1.js和entry3.js还共同引入了orgchart.js文件,打成了entry3~page1.js这个chunk

6、参考文章

理解webpack4.splitChunks